From 46242fb9a45b56752fe829d7a9e3c6d516ad637e Mon Sep 17 00:00:00 2001 From: Cyril TOVENA Date: Mon, 24 Dec 2018 20:23:10 -0400 Subject: [PATCH] Adds OpenCensus metrics integration. - First set of metrics added using informers cache. - Added flag for switching off metrics via helm and env vars. - Added a documentation section about metrics and usage. --- Gopkg.lock | 199 +- Gopkg.toml | 8 + README.md | 1 + build/Makefile | 5 +- build/helm.yaml | 21 +- cmd/controller/main.go | 61 +- docs/metrics.md | 78 + install/helm/README.md | 4 +- install/helm/agones/templates/controller.yaml | 9 +- install/helm/agones/templates/service.yaml | 9 +- install/helm/agones/values.yaml | 7 +- install/yaml/install.yaml | 12 +- pkg/metrics/controller.go | 359 +++ pkg/metrics/controller_test.go | 225 ++ pkg/metrics/doc.go | 16 + pkg/metrics/exporter.go | 47 + pkg/metrics/gameservers.go | 72 + pkg/metrics/metrics.go | 138 + pkg/metrics/util_test.go | 268 ++ vendor/github.com/google/uuid/CONTRIBUTING.md | 10 + vendor/github.com/google/uuid/CONTRIBUTORS | 9 + vendor/github.com/google/uuid/LICENSE | 27 + vendor/github.com/google/uuid/README.md | 19 + vendor/github.com/google/uuid/dce.go | 80 + vendor/github.com/google/uuid/doc.go | 12 + vendor/github.com/google/uuid/go.mod | 1 + vendor/github.com/google/uuid/hash.go | 53 + vendor/github.com/google/uuid/json_test.go | 62 + vendor/github.com/google/uuid/marshal.go | 37 + vendor/github.com/google/uuid/node.go | 89 + vendor/github.com/google/uuid/node_js.go | 12 + vendor/github.com/google/uuid/node_net.go | 33 + vendor/github.com/google/uuid/seq_test.go | 66 + vendor/github.com/google/uuid/sql.go | 59 + vendor/github.com/google/uuid/sql_test.go | 113 + vendor/github.com/google/uuid/time.go | 123 + vendor/github.com/google/uuid/util.go | 43 + vendor/github.com/google/uuid/uuid.go | 245 ++ vendor/github.com/google/uuid/uuid_test.go | 559 ++++ vendor/github.com/google/uuid/version1.go | 44 + vendor/github.com/google/uuid/version4.go | 38 + .../github.com/pborman/uuid/CONTRIBUTING.md | 10 + vendor/github.com/pborman/uuid/CONTRIBUTORS | 1 + vendor/github.com/pborman/uuid/LICENSE | 27 + vendor/github.com/pborman/uuid/README.md | 15 + vendor/github.com/pborman/uuid/dce.go | 84 + vendor/github.com/pborman/uuid/doc.go | 13 + vendor/github.com/pborman/uuid/hash.go | 53 + vendor/github.com/pborman/uuid/marshal.go | 85 + .../github.com/pborman/uuid/marshal_test.go | 124 + vendor/github.com/pborman/uuid/node.go | 50 + vendor/github.com/pborman/uuid/seq_test.go | 66 + vendor/github.com/pborman/uuid/sql.go | 68 + vendor/github.com/pborman/uuid/sql_test.go | 96 + vendor/github.com/pborman/uuid/time.go | 57 + vendor/github.com/pborman/uuid/util.go | 32 + vendor/github.com/pborman/uuid/uuid.go | 163 ++ vendor/github.com/pborman/uuid/uuid_test.go | 410 +++ vendor/github.com/pborman/uuid/version1.go | 23 + vendor/github.com/pborman/uuid/version4.go | 26 + .../prometheus/client_golang/.gitignore | 4 + .../prometheus/client_golang/AUTHORS.md | 18 - .../prometheus/client_golang/CHANGELOG.md | 72 + .../prometheus/client_golang/CONTRIBUTING.md | 8 +- .../prometheus/client_golang/Dockerfile | 23 + .../prometheus/client_golang/MAINTAINERS.md | 2 + .../prometheus/client_golang/Makefile | 32 + .../prometheus/client_golang/Makefile.common | 223 ++ .../prometheus/client_golang/README.md | 61 +- .../prometheus/client_golang/VERSION | 2 +- .../prometheus/client_golang/api/client.go | 131 + .../client_golang/api/client_test.go | 115 + .../client_golang/api/prometheus/api.go | 345 --- .../client_golang/api/prometheus/api_test.go | 453 --- .../client_golang/api/prometheus/v1/api.go | 519 ++++ .../api/prometheus/v1/api_test.go | 768 ++++++ .../client_golang/examples/random/main.go | 21 +- .../client_golang/examples/simple/main.go | 7 +- .../prometheus/client_golang/go.mod | 12 + .../prometheus/client_golang/go.sum | 16 + .../prometheus/benchmark_test.go | 24 +- .../client_golang/prometheus/collector.go | 73 +- .../prometheus/collector_test.go | 62 + .../client_golang/prometheus/counter.go | 191 +- .../client_golang/prometheus/counter_test.go | 160 +- .../client_golang/prometheus/desc.go | 51 +- .../client_golang/prometheus/desc_test.go | 30 + .../client_golang/prometheus/doc.go | 94 +- .../prometheus/example_clustermanager_test.go | 110 +- .../prometheus/example_timer_complex_test.go | 71 + .../prometheus/example_timer_gauge_test.go | 48 + .../prometheus/example_timer_test.go | 40 + .../client_golang/prometheus/examples_test.go | 103 +- .../prometheus/expvar_collector_test.go | 4 +- .../client_golang/prometheus/fnv.go | 13 + .../client_golang/prometheus/gauge.go | 204 +- .../client_golang/prometheus/gauge_test.go | 24 +- .../client_golang/prometheus/go_collector.go | 74 +- .../prometheus/go_collector_test.go | 119 +- .../prometheus/graphite/bridge.go | 282 ++ .../prometheus/graphite/bridge_test.go | 338 +++ .../client_golang/prometheus/histogram.go | 304 +- .../prometheus/histogram_test.go | 71 +- .../client_golang/prometheus/http.go | 252 +- .../client_golang/prometheus/http_test.go | 63 +- .../prometheus/internal/metric.go | 85 + .../client_golang/prometheus/labels.go | 87 + .../client_golang/prometheus/metric.go | 90 +- .../client_golang/prometheus/observer.go | 52 + .../prometheus/process_collector.go | 220 +- .../prometheus/process_collector_test.go | 77 +- .../client_golang/prometheus/promauto/auto.go | 223 ++ .../prometheus/promhttp/delegator.go | 199 ++ .../prometheus/promhttp/delegator_1_8.go | 181 ++ .../prometheus/promhttp/delegator_pre_1_8.go | 44 + .../client_golang/prometheus/promhttp/http.go | 250 +- .../prometheus/promhttp/http_test.go | 129 +- .../prometheus/promhttp/instrument_client.go | 97 + .../promhttp/instrument_client_1_8.go | 144 + .../promhttp/instrument_client_1_8_test.go | 195 ++ .../prometheus/promhttp/instrument_server.go | 447 +++ .../promhttp/instrument_server_test.go | 401 +++ .../prometheus/push/deprecated.go | 172 ++ .../push/example_add_from_gatherer_test.go | 80 + .../prometheus/push/examples_test.go | 35 +- .../client_golang/prometheus/push/push.go | 244 +- .../prometheus/push/push_test.go | 78 +- .../client_golang/prometheus/registry.go | 719 +++-- .../client_golang/prometheus/registry_test.go | 487 +++- .../client_golang/prometheus/summary.go | 194 +- .../client_golang/prometheus/summary_test.go | 78 +- .../prometheus/testutil/testutil.go | 189 ++ .../prometheus/testutil/testutil_test.go | 311 +++ .../client_golang/prometheus/timer.go | 54 + .../client_golang/prometheus/timer_test.go | 152 + .../client_golang/prometheus/untyped.go | 102 +- .../client_golang/prometheus/value.go | 94 +- .../client_golang/prometheus/value_test.go | 56 + .../client_golang/prometheus/vec.go | 494 ++-- .../client_golang/prometheus/vec_test.go | 281 +- .../client_golang/prometheus/wrap.go | 179 ++ .../client_golang/prometheus/wrap_test.go | 322 +++ vendor/go.opencensus.io/.gitignore | 9 + vendor/go.opencensus.io/AUTHORS | 1 + vendor/go.opencensus.io/CONTRIBUTING.md | 56 + vendor/go.opencensus.io/Gopkg.lock | 231 ++ vendor/go.opencensus.io/Gopkg.toml | 36 + vendor/go.opencensus.io/LICENSE | 202 ++ vendor/go.opencensus.io/README.md | 263 ++ vendor/go.opencensus.io/appveyor.yml | 24 + .../examples/exporter/exporter.go | 105 + .../go.opencensus.io/examples/grpc/README.md | 31 + .../examples/grpc/helloworld_client/main.go | 69 + .../examples/grpc/helloworld_server/main.go | 79 + .../examples/grpc/proto/helloworld.pb.go | 164 ++ .../examples/grpc/proto/helloworld.proto | 37 + .../examples/helloworld/main.go | 99 + .../go.opencensus.io/examples/http/README.md | 31 + .../examples/http/helloworld_client/main.go | 55 + .../examples/http/helloworld_server/main.go | 77 + .../examples/quickstart/stats.go | 164 ++ vendor/go.opencensus.io/exemplar/exemplar.go | 78 + .../go.opencensus.io/exporter/jaeger/agent.go | 89 + .../exporter/jaeger/example/main.go | 60 + .../exporter/jaeger/example_test.go | 74 + .../exporter/jaeger/internal/gen-go/README | 2 + .../gen-go/jaeger/GoUnusedProtection__.go | 6 + .../jaeger/internal/gen-go/jaeger/agent.go | 244 ++ .../collector-remote/collector-remote.go | 155 ++ .../internal/gen-go/jaeger/jaeger-consts.go | 23 + .../jaeger/internal/gen-go/jaeger/jaeger.go | 2443 +++++++++++++++++ .../exporter/jaeger/jaeger.go | 340 +++ .../exporter/jaeger/jaeger_test.go | 135 + .../exporter/prometheus/example/main.go | 83 + .../exporter/prometheus/example_test.go | 35 + .../exporter/prometheus/prometheus.go | 308 +++ .../exporter/prometheus/prometheus_test.go | 344 +++ .../exporter/stackdriver/propagation/http.go | 94 + .../stackdriver/propagation/http_test.go | 70 + .../exporter/zipkin/example/main.go | 77 + .../exporter/zipkin/example_test.go | 40 + .../exporter/zipkin/zipkin.go | 194 ++ .../exporter/zipkin/zipkin_test.go | 255 ++ vendor/go.opencensus.io/go.mod | 25 + vendor/go.opencensus.io/go.sum | 48 + .../internal/check/version.go | 88 + vendor/go.opencensus.io/internal/internal.go | 37 + .../internal/readme/README.md | 6 + .../internal/readme/mkdocs.sh | 3 + .../go.opencensus.io/internal/readme/stats.go | 56 + .../go.opencensus.io/internal/readme/tags.go | 60 + .../go.opencensus.io/internal/readme/trace.go | 32 + vendor/go.opencensus.io/internal/sanitize.go | 50 + .../internal/sanitize_test.go | 67 + .../internal/tagencoding/tagencoding.go | 72 + .../internal/testpb/generate.sh | 7 + .../go.opencensus.io/internal/testpb/impl.go | 93 + .../internal/testpb/test.pb.go | 228 ++ .../internal/testpb/test.proto | 16 + .../internal/traceinternals.go | 52 + vendor/go.opencensus.io/opencensus.go | 21 + .../plugin/ocgrpc/benchmark_test.go | 65 + .../go.opencensus.io/plugin/ocgrpc/client.go | 56 + .../plugin/ocgrpc/client_metrics.go | 107 + .../plugin/ocgrpc/client_spec_test.go | 152 + .../plugin/ocgrpc/client_stats_handler.go | 49 + .../ocgrpc/client_stats_handler_test.go | 345 +++ vendor/go.opencensus.io/plugin/ocgrpc/doc.go | 19 + .../plugin/ocgrpc/end_to_end_test.go | 239 ++ .../plugin/ocgrpc/example_test.go | 50 + .../plugin/ocgrpc/grpc_test.go | 139 + .../go.opencensus.io/plugin/ocgrpc/server.go | 80 + .../plugin/ocgrpc/server_metrics.go | 97 + .../plugin/ocgrpc/server_spec_test.go | 146 + .../plugin/ocgrpc/server_stats_handler.go | 63 + .../ocgrpc/server_stats_handler_test.go | 336 +++ .../plugin/ocgrpc/stats_common.go | 208 ++ .../plugin/ocgrpc/trace_common.go | 107 + .../plugin/ocgrpc/trace_common_test.go | 46 + .../plugin/ocgrpc/trace_test.go | 233 ++ .../go.opencensus.io/plugin/ochttp/client.go | 117 + .../plugin/ochttp/client_stats.go | 135 + .../plugin/ochttp/client_test.go | 316 +++ vendor/go.opencensus.io/plugin/ochttp/doc.go | 19 + .../plugin/ochttp/example_test.go | 77 + .../plugin/ochttp/propagation/b3/b3.go | 123 + .../plugin/ochttp/propagation/b3/b3_test.go | 210 ++ .../propagation/tracecontext/propagation.go | 187 ++ .../tracecontext/propagation_test.go | 267 ++ .../plugin/ochttp/propagation_test.go | 74 + .../go.opencensus.io/plugin/ochttp/route.go | 51 + .../plugin/ochttp/route_test.go | 80 + .../go.opencensus.io/plugin/ochttp/server.go | 440 +++ .../plugin/ochttp/server_test.go | 597 ++++ .../ochttp/span_annotating_client_trace.go | 169 ++ .../span_annotating_client_trace_test.go | 109 + .../go.opencensus.io/plugin/ochttp/stats.go | 265 ++ .../plugin/ochttp/stats_test.go | 88 + .../go.opencensus.io/plugin/ochttp/trace.go | 228 ++ .../plugin/ochttp/trace_test.go | 543 ++++ .../go.opencensus.io/stats/benchmark_test.go | 96 + vendor/go.opencensus.io/stats/doc.go | 69 + vendor/go.opencensus.io/stats/example_test.go | 33 + .../go.opencensus.io/stats/internal/record.go | 25 + .../stats/internal/validation.go | 28 + vendor/go.opencensus.io/stats/measure.go | 123 + .../go.opencensus.io/stats/measure_float64.go | 36 + .../go.opencensus.io/stats/measure_int64.go | 36 + vendor/go.opencensus.io/stats/record.go | 69 + vendor/go.opencensus.io/stats/units.go | 25 + .../stats/view/aggregation.go | 120 + .../stats/view/aggregation_data.go | 235 ++ .../stats/view/aggregation_data_test.go | 145 + .../stats/view/benchmark_test.go | 92 + .../go.opencensus.io/stats/view/collector.go | 87 + .../stats/view/collector_test.go | 118 + vendor/go.opencensus.io/stats/view/doc.go | 47 + .../stats/view/example_test.go | 39 + vendor/go.opencensus.io/stats/view/export.go | 58 + vendor/go.opencensus.io/stats/view/view.go | 185 ++ .../stats/view/view_measure_test.go | 50 + .../go.opencensus.io/stats/view/view_test.go | 446 +++ vendor/go.opencensus.io/stats/view/worker.go | 229 ++ .../stats/view/worker_commands.go | 183 ++ .../stats/view/worker_test.go | 435 +++ vendor/go.opencensus.io/tag/context.go | 67 + vendor/go.opencensus.io/tag/context_test.go | 44 + vendor/go.opencensus.io/tag/doc.go | 26 + vendor/go.opencensus.io/tag/example_test.go | 97 + vendor/go.opencensus.io/tag/key.go | 35 + vendor/go.opencensus.io/tag/map.go | 197 ++ vendor/go.opencensus.io/tag/map_codec.go | 234 ++ vendor/go.opencensus.io/tag/map_codec_test.go | 154 ++ vendor/go.opencensus.io/tag/map_test.go | 222 ++ vendor/go.opencensus.io/tag/profile_19.go | 31 + vendor/go.opencensus.io/tag/profile_not19.go | 23 + vendor/go.opencensus.io/tag/validate.go | 56 + vendor/go.opencensus.io/tag/validate_test.go | 110 + vendor/go.opencensus.io/trace/basetypes.go | 114 + .../go.opencensus.io/trace/benchmark_test.go | 105 + vendor/go.opencensus.io/trace/config.go | 48 + vendor/go.opencensus.io/trace/config_test.go | 33 + vendor/go.opencensus.io/trace/doc.go | 53 + .../go.opencensus.io/trace/examples_test.go | 41 + vendor/go.opencensus.io/trace/exemplar.go | 43 + .../go.opencensus.io/trace/exemplar_test.go | 100 + vendor/go.opencensus.io/trace/export.go | 90 + .../trace/internal/internal.go | 21 + .../trace/propagation/propagation.go | 108 + .../trace/propagation/propagation_test.go | 150 + vendor/go.opencensus.io/trace/sampling.go | 75 + vendor/go.opencensus.io/trace/spanbucket.go | 130 + vendor/go.opencensus.io/trace/spanstore.go | 306 +++ vendor/go.opencensus.io/trace/status_codes.go | 37 + vendor/go.opencensus.io/trace/trace.go | 516 ++++ vendor/go.opencensus.io/trace/trace_go11.go | 32 + .../go.opencensus.io/trace/trace_nongo11.go | 25 + vendor/go.opencensus.io/trace/trace_test.go | 714 +++++ .../trace/tracestate/tracestate.go | 147 + .../trace/tracestate/tracestate_test.go | 313 +++ .../go.opencensus.io/zpages/example_test.go | 28 + .../go.opencensus.io/zpages/formatter_test.go | 42 + .../go.opencensus.io/zpages/internal/gen.go | 19 + .../zpages/internal/public/opencensus.css | 0 .../zpages/internal/resources.go | 284 ++ .../zpages/internal/templates/footer.html | 2 + .../zpages/internal/templates/header.html | 12 + .../zpages/internal/templates/rpcz.html | 52 + .../zpages/internal/templates/summary.html | 43 + .../zpages/internal/templates/traces.html | 10 + vendor/go.opencensus.io/zpages/rpcz.go | 333 +++ vendor/go.opencensus.io/zpages/rpcz_test.go | 55 + vendor/go.opencensus.io/zpages/templates.go | 125 + .../go.opencensus.io/zpages/templates_test.go | 99 + vendor/go.opencensus.io/zpages/tracez.go | 442 +++ vendor/go.opencensus.io/zpages/zpages.go | 70 + vendor/go.opencensus.io/zpages/zpages_test.go | 129 + 317 files changed, 38581 insertions(+), 2700 deletions(-) create mode 100644 docs/metrics.md create mode 100644 pkg/metrics/controller.go create mode 100644 pkg/metrics/controller_test.go create mode 100644 pkg/metrics/doc.go create mode 100644 pkg/metrics/exporter.go create mode 100644 pkg/metrics/gameservers.go create mode 100644 pkg/metrics/metrics.go create mode 100644 pkg/metrics/util_test.go create mode 100644 vendor/github.com/google/uuid/CONTRIBUTING.md create mode 100644 vendor/github.com/google/uuid/CONTRIBUTORS create mode 100644 vendor/github.com/google/uuid/LICENSE create mode 100644 vendor/github.com/google/uuid/README.md create mode 100644 vendor/github.com/google/uuid/dce.go create mode 100644 vendor/github.com/google/uuid/doc.go create mode 100644 vendor/github.com/google/uuid/go.mod create mode 100644 vendor/github.com/google/uuid/hash.go create mode 100644 vendor/github.com/google/uuid/json_test.go create mode 100644 vendor/github.com/google/uuid/marshal.go create mode 100644 vendor/github.com/google/uuid/node.go create mode 100644 vendor/github.com/google/uuid/node_js.go create mode 100644 vendor/github.com/google/uuid/node_net.go create mode 100644 vendor/github.com/google/uuid/seq_test.go create mode 100644 vendor/github.com/google/uuid/sql.go create mode 100644 vendor/github.com/google/uuid/sql_test.go create mode 100644 vendor/github.com/google/uuid/time.go create mode 100644 vendor/github.com/google/uuid/util.go create mode 100644 vendor/github.com/google/uuid/uuid.go create mode 100644 vendor/github.com/google/uuid/uuid_test.go create mode 100644 vendor/github.com/google/uuid/version1.go create mode 100644 vendor/github.com/google/uuid/version4.go create mode 100644 vendor/github.com/pborman/uuid/CONTRIBUTING.md create mode 100644 vendor/github.com/pborman/uuid/CONTRIBUTORS create mode 100644 vendor/github.com/pborman/uuid/LICENSE create mode 100644 vendor/github.com/pborman/uuid/README.md create mode 100644 vendor/github.com/pborman/uuid/dce.go create mode 100644 vendor/github.com/pborman/uuid/doc.go create mode 100644 vendor/github.com/pborman/uuid/hash.go create mode 100644 vendor/github.com/pborman/uuid/marshal.go create mode 100644 vendor/github.com/pborman/uuid/marshal_test.go create mode 100644 vendor/github.com/pborman/uuid/node.go create mode 100644 vendor/github.com/pborman/uuid/seq_test.go create mode 100644 vendor/github.com/pborman/uuid/sql.go create mode 100644 vendor/github.com/pborman/uuid/sql_test.go create mode 100644 vendor/github.com/pborman/uuid/time.go create mode 100644 vendor/github.com/pborman/uuid/util.go create mode 100644 vendor/github.com/pborman/uuid/uuid.go create mode 100644 vendor/github.com/pborman/uuid/uuid_test.go create mode 100644 vendor/github.com/pborman/uuid/version1.go create mode 100644 vendor/github.com/pborman/uuid/version4.go delete mode 100644 vendor/github.com/prometheus/client_golang/AUTHORS.md create mode 100644 vendor/github.com/prometheus/client_golang/Dockerfile create mode 100644 vendor/github.com/prometheus/client_golang/MAINTAINERS.md create mode 100644 vendor/github.com/prometheus/client_golang/Makefile create mode 100644 vendor/github.com/prometheus/client_golang/Makefile.common create mode 100644 vendor/github.com/prometheus/client_golang/api/client.go create mode 100644 vendor/github.com/prometheus/client_golang/api/client_test.go delete mode 100644 vendor/github.com/prometheus/client_golang/api/prometheus/api.go delete mode 100644 vendor/github.com/prometheus/client_golang/api/prometheus/api_test.go create mode 100644 vendor/github.com/prometheus/client_golang/api/prometheus/v1/api.go create mode 100644 vendor/github.com/prometheus/client_golang/api/prometheus/v1/api_test.go create mode 100644 vendor/github.com/prometheus/client_golang/go.mod create mode 100644 vendor/github.com/prometheus/client_golang/go.sum create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/collector_test.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/desc_test.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/example_timer_complex_test.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/example_timer_gauge_test.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/example_timer_test.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/graphite/bridge.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/graphite/bridge_test.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/internal/metric.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/labels.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/observer.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/promauto/auto.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/promhttp/delegator.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/promhttp/delegator_1_8.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/promhttp/delegator_pre_1_8.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client_1_8.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client_1_8_test.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_server.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_server_test.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/push/deprecated.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/push/example_add_from_gatherer_test.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/testutil/testutil.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/testutil/testutil_test.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/timer.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/timer_test.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/value_test.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/wrap.go create mode 100644 vendor/github.com/prometheus/client_golang/prometheus/wrap_test.go create mode 100644 vendor/go.opencensus.io/.gitignore create mode 100644 vendor/go.opencensus.io/AUTHORS create mode 100644 vendor/go.opencensus.io/CONTRIBUTING.md create mode 100644 vendor/go.opencensus.io/Gopkg.lock create mode 100644 vendor/go.opencensus.io/Gopkg.toml create mode 100644 vendor/go.opencensus.io/LICENSE create mode 100644 vendor/go.opencensus.io/README.md create mode 100644 vendor/go.opencensus.io/appveyor.yml create mode 100644 vendor/go.opencensus.io/examples/exporter/exporter.go create mode 100644 vendor/go.opencensus.io/examples/grpc/README.md create mode 100644 vendor/go.opencensus.io/examples/grpc/helloworld_client/main.go create mode 100644 vendor/go.opencensus.io/examples/grpc/helloworld_server/main.go create mode 100644 vendor/go.opencensus.io/examples/grpc/proto/helloworld.pb.go create mode 100644 vendor/go.opencensus.io/examples/grpc/proto/helloworld.proto create mode 100644 vendor/go.opencensus.io/examples/helloworld/main.go create mode 100644 vendor/go.opencensus.io/examples/http/README.md create mode 100644 vendor/go.opencensus.io/examples/http/helloworld_client/main.go create mode 100644 vendor/go.opencensus.io/examples/http/helloworld_server/main.go create mode 100644 vendor/go.opencensus.io/examples/quickstart/stats.go create mode 100644 vendor/go.opencensus.io/exemplar/exemplar.go create mode 100644 vendor/go.opencensus.io/exporter/jaeger/agent.go create mode 100644 vendor/go.opencensus.io/exporter/jaeger/example/main.go create mode 100644 vendor/go.opencensus.io/exporter/jaeger/example_test.go create mode 100644 vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/README create mode 100644 vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/GoUnusedProtection__.go create mode 100644 vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/agent.go create mode 100755 vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/collector-remote/collector-remote.go create mode 100644 vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/jaeger-consts.go create mode 100644 vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/jaeger.go create mode 100644 vendor/go.opencensus.io/exporter/jaeger/jaeger.go create mode 100644 vendor/go.opencensus.io/exporter/jaeger/jaeger_test.go create mode 100644 vendor/go.opencensus.io/exporter/prometheus/example/main.go create mode 100644 vendor/go.opencensus.io/exporter/prometheus/example_test.go create mode 100644 vendor/go.opencensus.io/exporter/prometheus/prometheus.go create mode 100644 vendor/go.opencensus.io/exporter/prometheus/prometheus_test.go create mode 100644 vendor/go.opencensus.io/exporter/stackdriver/propagation/http.go create mode 100644 vendor/go.opencensus.io/exporter/stackdriver/propagation/http_test.go create mode 100644 vendor/go.opencensus.io/exporter/zipkin/example/main.go create mode 100644 vendor/go.opencensus.io/exporter/zipkin/example_test.go create mode 100644 vendor/go.opencensus.io/exporter/zipkin/zipkin.go create mode 100644 vendor/go.opencensus.io/exporter/zipkin/zipkin_test.go create mode 100644 vendor/go.opencensus.io/go.mod create mode 100644 vendor/go.opencensus.io/go.sum create mode 100644 vendor/go.opencensus.io/internal/check/version.go create mode 100644 vendor/go.opencensus.io/internal/internal.go create mode 100644 vendor/go.opencensus.io/internal/readme/README.md create mode 100755 vendor/go.opencensus.io/internal/readme/mkdocs.sh create mode 100644 vendor/go.opencensus.io/internal/readme/stats.go create mode 100644 vendor/go.opencensus.io/internal/readme/tags.go create mode 100644 vendor/go.opencensus.io/internal/readme/trace.go create mode 100644 vendor/go.opencensus.io/internal/sanitize.go create mode 100644 vendor/go.opencensus.io/internal/sanitize_test.go create mode 100644 vendor/go.opencensus.io/internal/tagencoding/tagencoding.go create mode 100755 vendor/go.opencensus.io/internal/testpb/generate.sh create mode 100644 vendor/go.opencensus.io/internal/testpb/impl.go create mode 100644 vendor/go.opencensus.io/internal/testpb/test.pb.go create mode 100644 vendor/go.opencensus.io/internal/testpb/test.proto create mode 100644 vendor/go.opencensus.io/internal/traceinternals.go create mode 100644 vendor/go.opencensus.io/opencensus.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/benchmark_test.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/client.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/client_metrics.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/client_spec_test.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/client_stats_handler.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/client_stats_handler_test.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/doc.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/end_to_end_test.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/example_test.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/grpc_test.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/server.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/server_metrics.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/server_spec_test.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/server_stats_handler.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/server_stats_handler_test.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/stats_common.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/trace_common.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/trace_common_test.go create mode 100644 vendor/go.opencensus.io/plugin/ocgrpc/trace_test.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/client.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/client_stats.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/client_test.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/doc.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/example_test.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/propagation/b3/b3.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/propagation/b3/b3_test.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/propagation/tracecontext/propagation.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/propagation/tracecontext/propagation_test.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/propagation_test.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/route.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/route_test.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/server.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/server_test.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/span_annotating_client_trace.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/span_annotating_client_trace_test.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/stats.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/stats_test.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/trace.go create mode 100644 vendor/go.opencensus.io/plugin/ochttp/trace_test.go create mode 100644 vendor/go.opencensus.io/stats/benchmark_test.go create mode 100644 vendor/go.opencensus.io/stats/doc.go create mode 100644 vendor/go.opencensus.io/stats/example_test.go create mode 100644 vendor/go.opencensus.io/stats/internal/record.go create mode 100644 vendor/go.opencensus.io/stats/internal/validation.go create mode 100644 vendor/go.opencensus.io/stats/measure.go create mode 100644 vendor/go.opencensus.io/stats/measure_float64.go create mode 100644 vendor/go.opencensus.io/stats/measure_int64.go create mode 100644 vendor/go.opencensus.io/stats/record.go create mode 100644 vendor/go.opencensus.io/stats/units.go create mode 100644 vendor/go.opencensus.io/stats/view/aggregation.go create mode 100644 vendor/go.opencensus.io/stats/view/aggregation_data.go create mode 100644 vendor/go.opencensus.io/stats/view/aggregation_data_test.go create mode 100644 vendor/go.opencensus.io/stats/view/benchmark_test.go create mode 100644 vendor/go.opencensus.io/stats/view/collector.go create mode 100644 vendor/go.opencensus.io/stats/view/collector_test.go create mode 100644 vendor/go.opencensus.io/stats/view/doc.go create mode 100644 vendor/go.opencensus.io/stats/view/example_test.go create mode 100644 vendor/go.opencensus.io/stats/view/export.go create mode 100644 vendor/go.opencensus.io/stats/view/view.go create mode 100644 vendor/go.opencensus.io/stats/view/view_measure_test.go create mode 100644 vendor/go.opencensus.io/stats/view/view_test.go create mode 100644 vendor/go.opencensus.io/stats/view/worker.go create mode 100644 vendor/go.opencensus.io/stats/view/worker_commands.go create mode 100644 vendor/go.opencensus.io/stats/view/worker_test.go create mode 100644 vendor/go.opencensus.io/tag/context.go create mode 100644 vendor/go.opencensus.io/tag/context_test.go create mode 100644 vendor/go.opencensus.io/tag/doc.go create mode 100644 vendor/go.opencensus.io/tag/example_test.go create mode 100644 vendor/go.opencensus.io/tag/key.go create mode 100644 vendor/go.opencensus.io/tag/map.go create mode 100644 vendor/go.opencensus.io/tag/map_codec.go create mode 100644 vendor/go.opencensus.io/tag/map_codec_test.go create mode 100644 vendor/go.opencensus.io/tag/map_test.go create mode 100644 vendor/go.opencensus.io/tag/profile_19.go create mode 100644 vendor/go.opencensus.io/tag/profile_not19.go create mode 100644 vendor/go.opencensus.io/tag/validate.go create mode 100644 vendor/go.opencensus.io/tag/validate_test.go create mode 100644 vendor/go.opencensus.io/trace/basetypes.go create mode 100644 vendor/go.opencensus.io/trace/benchmark_test.go create mode 100644 vendor/go.opencensus.io/trace/config.go create mode 100644 vendor/go.opencensus.io/trace/config_test.go create mode 100644 vendor/go.opencensus.io/trace/doc.go create mode 100644 vendor/go.opencensus.io/trace/examples_test.go create mode 100644 vendor/go.opencensus.io/trace/exemplar.go create mode 100644 vendor/go.opencensus.io/trace/exemplar_test.go create mode 100644 vendor/go.opencensus.io/trace/export.go create mode 100644 vendor/go.opencensus.io/trace/internal/internal.go create mode 100644 vendor/go.opencensus.io/trace/propagation/propagation.go create mode 100644 vendor/go.opencensus.io/trace/propagation/propagation_test.go create mode 100644 vendor/go.opencensus.io/trace/sampling.go create mode 100644 vendor/go.opencensus.io/trace/spanbucket.go create mode 100644 vendor/go.opencensus.io/trace/spanstore.go create mode 100644 vendor/go.opencensus.io/trace/status_codes.go create mode 100644 vendor/go.opencensus.io/trace/trace.go create mode 100644 vendor/go.opencensus.io/trace/trace_go11.go create mode 100644 vendor/go.opencensus.io/trace/trace_nongo11.go create mode 100644 vendor/go.opencensus.io/trace/trace_test.go create mode 100644 vendor/go.opencensus.io/trace/tracestate/tracestate.go create mode 100644 vendor/go.opencensus.io/trace/tracestate/tracestate_test.go create mode 100644 vendor/go.opencensus.io/zpages/example_test.go create mode 100644 vendor/go.opencensus.io/zpages/formatter_test.go create mode 100644 vendor/go.opencensus.io/zpages/internal/gen.go create mode 100644 vendor/go.opencensus.io/zpages/internal/public/opencensus.css create mode 100644 vendor/go.opencensus.io/zpages/internal/resources.go create mode 100644 vendor/go.opencensus.io/zpages/internal/templates/footer.html create mode 100644 vendor/go.opencensus.io/zpages/internal/templates/header.html create mode 100644 vendor/go.opencensus.io/zpages/internal/templates/rpcz.html create mode 100644 vendor/go.opencensus.io/zpages/internal/templates/summary.html create mode 100644 vendor/go.opencensus.io/zpages/internal/templates/traces.html create mode 100644 vendor/go.opencensus.io/zpages/rpcz.go create mode 100644 vendor/go.opencensus.io/zpages/rpcz_test.go create mode 100644 vendor/go.opencensus.io/zpages/templates.go create mode 100644 vendor/go.opencensus.io/zpages/templates_test.go create mode 100644 vendor/go.opencensus.io/zpages/tracez.go create mode 100644 vendor/go.opencensus.io/zpages/zpages.go create mode 100644 vendor/go.opencensus.io/zpages/zpages_test.go diff --git a/Gopkg.lock b/Gopkg.lock index bb9c5ceaff..f66699c0fa 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,74 +2,57 @@ [[projects]] - digest = "1:2b10b9a545074605403d32baf9dda24b7582976ba7e9b46c4c7b9da9edac03e7" name = "cloud.google.com/go" packages = ["compute/metadata"] - pruneopts = "" revision = "aad3f485ee528456e0768f20397b4d9dd941e755" version = "v0.25.0" [[projects]] branch = "master" - digest = "1:0c5485088ce274fac2e931c1b979f2619345097b39d91af3239977114adf0320" name = "github.com/beorn7/perks" packages = ["quantile"] - pruneopts = "" revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" [[projects]] - digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b" name = "github.com/davecgh/go-spew" packages = ["spew"] - pruneopts = "" revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" [[projects]] - digest = "1:9f1e571696860f2b4f8a241b43ce91c6085e7aaed849ccca53f590a4dc7b95bd" name = "github.com/fsnotify/fsnotify" packages = ["."] - pruneopts = "" revision = "629574ca2a5df945712d3079857300b5e4da0236" version = "v1.4.2" [[projects]] - digest = "1:b13707423743d41665fd23f0c36b2f37bb49c30e94adb813319c44188a51ba22" name = "github.com/ghodss/yaml" packages = ["."] - pruneopts = "" revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" version = "v1.0.0" [[projects]] - digest = "1:70a80170917a15e1ff02faab5f9e716e945e0676e86599ba144d38f96e30c3bf" name = "github.com/gogo/protobuf" packages = [ "proto", - "sortkeys", + "sortkeys" ] - pruneopts = "" revision = "342cbe0a04158f6dcb03ca0079991a51a4248c02" version = "v0.5" [[projects]] branch = "master" - digest = "1:107b233e45174dbab5b1324201d092ea9448e58243ab9f039e4c0f332e121e3a" name = "github.com/golang/glog" packages = ["."] - pruneopts = "" revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" [[projects]] branch = "master" - digest = "1:736952736991249599ad037f945ba3d45f6bc5d72e73d84203560745624552d1" name = "github.com/golang/groupcache" packages = ["lru"] - pruneopts = "" revision = "84a468cf14b4376def5d68c722b139b881c450a4" [[projects]] - digest = "1:3dd078fda7500c341bc26cfbc6c6a34614f295a2457149fc1045cab767cbcf18" name = "github.com/golang/protobuf" packages = [ "jsonpb", @@ -79,77 +62,69 @@ "ptypes/any", "ptypes/duration", "ptypes/struct", - "ptypes/timestamp", + "ptypes/timestamp" ] - pruneopts = "" revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5" version = "v1.2.0" [[projects]] branch = "master" - digest = "1:1e5b1e14524ed08301977b7b8e10c719ed853cbf3f24ecb66fae783a46f207a6" name = "github.com/google/btree" packages = ["."] - pruneopts = "" revision = "4030bb1f1f0c35b30ca7009e9ebd06849dd45306" [[projects]] branch = "master" - digest = "1:754f77e9c839b24778a4b64422236d38515301d2baeb63113aa3edc42e6af692" name = "github.com/google/gofuzz" packages = ["."] - pruneopts = "" revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1" [[projects]] - digest = "1:2a131706ff80636629ab6373f2944569b8252ecc018cda8040931b05d32e3c16" + name = "github.com/google/uuid" + packages = ["."] + revision = "9b3b1e0f5f99ae461456d768e7d301a7acdaa2d8" + version = "v1.1.0" + +[[projects]] name = "github.com/googleapis/gnostic" packages = [ "OpenAPIv2", "compiler", - "extensions", + "extensions" ] - pruneopts = "" revision = "ee43cbb60db7bd22502942cccbc39059117352ab" version = "v0.1.0" [[projects]] branch = "master" - digest = "1:5e345eb75d8bfb2b91cfbfe02a82a79c0b2ea55cf06c5a4d180a9321f36973b4" name = "github.com/gregjones/httpcache" packages = [ ".", - "diskcache", + "diskcache" ] - pruneopts = "" revision = "c63ab54fda8f77302f8d414e19933f2b6026a089" [[projects]] - digest = "1:dc70bd0ecd18729cf0ecf8828045e185ce0b15084f1bbe250422ab65e6fcd79c" name = "github.com/grpc-ecosystem/grpc-gateway" packages = [ "runtime", "runtime/internal", - "utilities", + "utilities" ] - pruneopts = "" revision = "aeab1d96e0f1368d243e2e5f526aa29d495517bb" version = "v1.5.1" [[projects]] branch = "master" - digest = "1:43987212a2f16bfacc1a286e9118f212d60c136ed53c6c9477c18921db53140b" name = "github.com/hashicorp/golang-lru" packages = [ ".", - "simplelru", + "simplelru" ] - pruneopts = "" revision = "0a025b7e63adc15a622f29b0b2c4c3848243bbf6" [[projects]] branch = "master" - digest = "1:147d671753effde6d3bcd58fc74c1d67d740196c84c280c762a5417319499972" name = "github.com/hashicorp/hcl" packages = [ ".", @@ -161,24 +136,19 @@ "hcl/token", "json/parser", "json/scanner", - "json/token", + "json/token" ] - pruneopts = "" revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" [[projects]] branch = "master" - digest = "1:241608ad91b91d57bf294e028b678131636d483e2da17ca68e8fe2feb3454995" name = "github.com/heptiolabs/healthcheck" packages = ["."] - pruneopts = "" revision = "da5fdee475fb09de87c5d4a50da233b7d28767b1" [[projects]] - digest = "1:302c6eb8e669c997bec516a138b8fc496018faa1ece4c13e445a2749fbe079bb" name = "github.com/imdario/mergo" packages = ["."] - pruneopts = "" revision = "9316a62528ac99aaecb4e47eadd6dc8aa6533d58" version = "v0.3.5" @@ -194,217 +164,194 @@ digest = "1:b79fc583e4dc7055ed86742e22164ac41bf8c0940722dbcb600f1a3ace1a8cb5" name = "github.com/json-iterator/go" packages = ["."] - pruneopts = "" revision = "1624edc4454b8682399def8740d46db5e4362ba4" version = "v1.1.5" [[projects]] - digest = "1:6a874e3ddfb9db2b42bd8c85b6875407c702fa868eed20634ff489bc896ccfd3" name = "github.com/konsorten/go-windows-terminal-sequences" packages = ["."] - pruneopts = "" revision = "5c8c8bd35d3832f5d134ae1e1e375b69a4d25242" version = "v1.0.1" [[projects]] - digest = "1:1ce378ab2352c756c6d7a0172c22ecbd387659d32712a4ce3bc474273309a5dc" name = "github.com/magiconair/properties" packages = ["."] - pruneopts = "" revision = "be5ece7dd465ab0765a9682137865547526d1dfb" version = "v1.7.3" [[projects]] branch = "master" - digest = "1:58050e2bc9621cc6b68c1da3e4a0d1c40ad1f89062b9855c26521fd42a97a106" name = "github.com/mattbaird/jsonpatch" packages = ["."] - pruneopts = "" revision = "81af80346b1a01caae0cbc27fd3c1ba5b11e189f" [[projects]] - digest = "1:4c23ced97a470b17d9ffd788310502a077b9c1f60221a85563e49696276b4147" name = "github.com/matttproud/golang_protobuf_extensions" packages = ["pbutil"] - pruneopts = "" revision = "3247c84500bff8d9fb6d579d800f20b3e091582c" version = "v1.0.0" [[projects]] branch = "master" - digest = "1:30a2adc78c422ebd23aac9cfece529954d5eacf9ddbe37345f2a17439f8fa849" name = "github.com/mitchellh/mapstructure" packages = ["."] - pruneopts = "" revision = "06020f85339e21b2478f756a78e295255ffa4d6a" [[projects]] - digest = "1:0c0ff2a89c1bb0d01887e1dac043ad7efbf3ec77482ef058ac423d13497e16fd" name = "github.com/modern-go/concurrent" packages = ["."] - pruneopts = "" revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94" version = "1.0.3" [[projects]] - digest = "1:e32bdbdb7c377a07a9a46378290059822efdce5c8d96fe71940d87cb4f918855" name = "github.com/modern-go/reflect2" packages = ["."] - pruneopts = "" revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd" version = "1.0.1" [[projects]] - digest = "1:9c740db1f7015dffa093aa5c70862d277fe49f5e92b56ca5d0d69ef0e37c01db" + name = "github.com/pborman/uuid" + packages = ["."] + revision = "adf5a7427709b9deb95d29d3fa8a2bf9cfd388f1" + version = "v1.2" + +[[projects]] name = "github.com/pelletier/go-toml" packages = ["."] - pruneopts = "" revision = "16398bac157da96aa88f98a2df640c7f32af1da2" version = "v1.0.1" [[projects]] branch = "master" - digest = "1:c24598ffeadd2762552269271b3b1510df2d83ee6696c1e543a0ff653af494bc" name = "github.com/petar/GoLLRB" packages = ["llrb"] - pruneopts = "" revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4" [[projects]] - digest = "1:b46305723171710475f2dd37547edd57b67b9de9f2a6267cafdd98331fd6897f" name = "github.com/peterbourgon/diskv" packages = ["."] - pruneopts = "" revision = "5f041e8faa004a95c88a202771f4cc3e991971e6" version = "v2.0.1" [[projects]] - digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca" name = "github.com/pkg/errors" packages = ["."] - pruneopts = "" revision = "645ef00459ed84a119197bfb8d8205042c6df63d" version = "v0.8.0" [[projects]] - digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411" name = "github.com/pmezard/go-difflib" packages = ["difflib"] - pruneopts = "" revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" [[projects]] - digest = "1:4142d94383572e74b42352273652c62afec5b23f325222ed09198f46009022d1" name = "github.com/prometheus/client_golang" - packages = ["prometheus"] - pruneopts = "" - revision = "c5b7fccd204277076155f10851dad72b76a49317" - version = "v0.8.0" + packages = [ + "prometheus", + "prometheus/internal", + "prometheus/promhttp", + "prometheus/testutil" + ] + revision = "505eaef017263e299324067d40ca2c48f6a2cf50" + version = "v0.9.2" [[projects]] branch = "master" - digest = "1:60aca47f4eeeb972f1b9da7e7db51dee15ff6c59f7b401c1588b8e6771ba15ef" name = "github.com/prometheus/client_model" packages = ["go"] - pruneopts = "" revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c" [[projects]] branch = "master" - digest = "1:acf9415ef3a5f298495b0e1aa4d0e18f571a3c845872944e6c52777496819b21" name = "github.com/prometheus/common" packages = [ "expfmt", "internal/bitbucket.org/ww/goautoneg", - "model", + "model" ] - pruneopts = "" revision = "89604d197083d4781071d3c65855d24ecfb0a563" [[projects]] branch = "master" - digest = "1:8e7ce3f6496b94d1028a7cb91943c2243351e375e0979d988f8427acfe701280" name = "github.com/prometheus/procfs" packages = [ ".", "internal/util", "nfs", - "xfs", + "xfs" ] - pruneopts = "" revision = "282c8707aa210456a825798969cc27edda34992a" [[projects]] - digest = "1:9d57e200ef5ccc4217fe0a34287308bac652435e7c6513f6263e0493d2245c56" name = "github.com/sirupsen/logrus" packages = ["."] - pruneopts = "" revision = "bcd833dfe83d3cebad139e4a29ed79cb2318bf95" version = "v1.2.0" [[projects]] - digest = "1:d1c1e5f4064b332bd90bba7bc2ab403c0afd7b36d34b7875877a45c78d24f5c1" name = "github.com/spf13/afero" packages = [ ".", - "mem", + "mem" ] - pruneopts = "" revision = "8d919cbe7e2627e417f3e45c3c0e489a5b7e2536" version = "v1.0.0" [[projects]] - digest = "1:6ff9b74bfea2625f805edec59395dc37e4a06458dd3c14e3372337e3d35a2ed6" name = "github.com/spf13/cast" packages = ["."] - pruneopts = "" revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4" version = "v1.1.0" [[projects]] branch = "master" - digest = "1:5cb42b990db5dc48b8bc23b6ee77b260713ba3244ca495cd1ed89533dc482a49" name = "github.com/spf13/jwalterweatherman" packages = ["."] - pruneopts = "" revision = "12bd96e66386c1960ab0f74ced1362f66f552f7b" [[projects]] - digest = "1:261bc565833ef4f02121450d74eb88d5ae4bd74bfe5d0e862cddb8550ec35000" name = "github.com/spf13/pflag" packages = ["."] - pruneopts = "" revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" version = "v1.0.0" [[projects]] - digest = "1:b98ee2c3f1469ab3b494859d3a9c9e595ec2c63660dea130a5154cfcd427b608" name = "github.com/spf13/viper" packages = ["."] - pruneopts = "" revision = "6d33b5a963d922d182c91e8a1c88d81fd150cfd4" version = "v1.3.1" [[projects]] - digest = "1:c587772fb8ad29ad4db67575dad25ba17a51f072ff18a22b4f0257a4d9c24f75" name = "github.com/stretchr/testify" packages = ["assert"] - pruneopts = "" revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" version = "v1.2.2" +[[projects]] + name = "go.opencensus.io" + packages = [ + ".", + "exemplar", + "exporter/prometheus", + "internal", + "internal/tagencoding", + "stats", + "stats/internal", + "stats/view", + "tag" + ] + revision = "b7bf3cdb64150a8c8c53b769fdeb2ba581bd4d4b" + version = "v0.18.0" + [[projects]] branch = "master" - digest = "1:a992f0c68fa56538ede286e9e3827341fab57b7532552a771f47a32db0e6117b" name = "golang.org/x/crypto" packages = ["ssh/terminal"] - pruneopts = "" revision = "bd6f299fb381e4c3393d1c4b1f0b94f5e77650c8" [[projects]] branch = "master" - digest = "1:e3fd71c3687fb1d263e491fc3bd9013858aeb30a6393fc9b77cbbdc37d0f9727" name = "golang.org/x/net" packages = [ "context", @@ -414,38 +361,32 @@ "idna", "internal/timeseries", "lex/httplex", - "trace", + "trace" ] - pruneopts = "" revision = "c73622c77280266305273cb545f54516ced95b93" [[projects]] branch = "master" - digest = "1:a6237bd1fe4eed8524971a2923fcadb54e9584b19516a35d3f76f1c53f603cf9" name = "golang.org/x/oauth2" packages = [ ".", "google", "internal", "jws", - "jwt", + "jwt" ] - pruneopts = "" revision = "ef147856a6ddbb60760db74283d2424e98c87bff" [[projects]] - digest = "1:f358024b019f87eecaadcb098113a40852c94fe58ea670ef3c3e2d2c7bd93db1" name = "golang.org/x/sys" packages = [ "unix", - "windows", + "windows" ] - pruneopts = "" revision = "4ed8d59d0b35e1e29334a206d1b3f38b1e5dfb31" [[projects]] branch = "master" - digest = "1:1d7891b3bc073ada6c9808c424133e01ded2a1ccb1679e0c6aabbff5b70bd447" name = "golang.org/x/text" packages = [ "collate", @@ -461,21 +402,17 @@ "unicode/bidi", "unicode/cldr", "unicode/norm", - "unicode/rangetable", + "unicode/rangetable" ] - pruneopts = "" revision = "3cfc6c0d6bf80d724d98c26c4822f4a35d37598e" [[projects]] branch = "master" - digest = "1:55a681cb66f28755765fa5fa5104cbd8dc85c55c02d206f9f89566451e3fe1aa" name = "golang.org/x/time" packages = ["rate"] - pruneopts = "" revision = "fbb02b2291d28baffd63558aa44b4b56f178d650" [[projects]] - digest = "1:c1771ca6060335f9768dff6558108bc5ef6c58506821ad43377ee23ff059e472" name = "google.golang.org/appengine" packages = [ ".", @@ -487,25 +424,21 @@ "internal/modules", "internal/remote_api", "internal/urlfetch", - "urlfetch", + "urlfetch" ] - pruneopts = "" revision = "b1f26356af11148e710935ed1ac8a7f5702c7612" version = "v1.1.0" [[projects]] branch = "master" - digest = "1:6c15114fafeac4c833544476dec4207a3476799d50ab5165af79eb97806884cf" name = "google.golang.org/genproto" packages = [ "googleapis/api/annotations", - "googleapis/rpc/status", + "googleapis/rpc/status" ] - pruneopts = "" revision = "7f0da29060c682909f650ad8ed4e515bd74fa12a" [[projects]] - digest = "1:1293087271e314cfa2b3decededba2ecba0ff327e7b7809e00f73f616449191c" name = "google.golang.org/grpc" packages = [ ".", @@ -533,30 +466,24 @@ "resolver/passthrough", "stats", "status", - "tap", + "tap" ] - pruneopts = "" revision = "2e463a05d100327ca47ac218281906921038fd95" version = "v1.16.0" [[projects]] - digest = "1:e5d1fb981765b6f7513f793a3fcaac7158408cca77f75f7311ac82cc88e9c445" name = "gopkg.in/inf.v0" packages = ["."] - pruneopts = "" revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4" version = "v0.9.0" [[projects]] branch = "v2" - digest = "1:81314a486195626940617e43740b4fa073f265b0715c9f54ce2027fee1cb5f61" name = "gopkg.in/yaml.v2" packages = ["."] - pruneopts = "" revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f" [[projects]] - digest = "1:262c8fc3b6a9f853ae05e3f92d9161f7ca164433f9279f2cbe745832a81481f3" name = "k8s.io/api" packages = [ "admission/v1beta1", @@ -588,14 +515,12 @@ "settings/v1alpha1", "storage/v1", "storage/v1alpha1", - "storage/v1beta1", + "storage/v1beta1" ] - pruneopts = "" revision = "95336914c664590c72e583850a44daa9520cc1d6" version = "kubernetes-1.11.5" [[projects]] - digest = "1:a81e182e669f129adb597f569d701fb68158cdd38720329df587c29e36992ebc" name = "k8s.io/apiextensions-apiserver" packages = [ "pkg/apis/apiextensions", @@ -604,14 +529,12 @@ "pkg/client/clientset/clientset/fake", "pkg/client/clientset/clientset/scheme", "pkg/client/clientset/clientset/typed/apiextensions/v1beta1", - "pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake", + "pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake" ] - pruneopts = "" revision = "57b8dbfcc51afd83583adc319a3d94bac32d013b" version = "kubernetes-1.11.5" [[projects]] - digest = "1:c3bdd51f1e133343b06e32ede8018227e49651ab3a591f7e0372f3506b484d1f" name = "k8s.io/apimachinery" packages = [ "pkg/api/equality", @@ -645,9 +568,11 @@ "pkg/util/json", "pkg/util/mergepatch", "pkg/util/net", + "pkg/util/rand", "pkg/util/runtime", "pkg/util/sets", "pkg/util/strategicpatch", + "pkg/util/uuid", "pkg/util/validation", "pkg/util/validation/field", "pkg/util/wait", @@ -655,14 +580,12 @@ "pkg/version", "pkg/watch", "third_party/forked/golang/json", - "third_party/forked/golang/reflect", + "third_party/forked/golang/reflect" ] - pruneopts = "" revision = "70adfbae261eebb795b76321790745ad0e3c523f" version = "kubernetes-1.11.5" [[projects]] - digest = "1:d04779a8de7d5465e0463bd986506348de5e89677c74777f695d3145a7a8d15e" name = "k8s.io/client-go" packages = [ "discovery", @@ -823,18 +746,15 @@ "util/integer", "util/jsonpath", "util/retry", - "util/workqueue", + "util/workqueue" ] - pruneopts = "" revision = "7d04d0e2a0a1a4d4a1cd6baa432a2301492e4e65" version = "v8.0.0" [[projects]] branch = "master" - digest = "1:b3176bd656f9b79385d4c8f98b37b400715e1f580d27092cf72be0573680d718" name = "k8s.io/kube-openapi" packages = ["pkg/util/proto"] - pruneopts = "" revision = "d52097ab4580a8f654862188cd66db48e87f62a3" [solve-meta] @@ -902,5 +822,6 @@ "k8s.io/client-go/util/flowcontrol", "k8s.io/client-go/util/workqueue", ] + inputs-digest = "fb768505af999cd425bfccb625449ff6a2d08736fe75184043e0dc22e59a4b5e" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 7af1a8a1d5..636a24bdfd 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -83,3 +83,11 @@ [[constraint]] branch = "master" name = "github.com/joonix/log" + +[[constraint]] + name = "go.opencensus.io" + version = "0.18.0" + +[[override]] + name = "github.com/prometheus/client_golang" + version = "0.9.2" diff --git a/README.md b/README.md index 5ee6d116f8..b48269981f 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ Documentation and usage guides on how to develop and host dedicated game servers - [Game Server Specification](./docs/gameserver_spec.md) - [Fleet Specification](./docs/fleet_spec.md) - [Fleet Autoscaler Specification](./docs/fleetautoscaler_spec.md) +- [Metrics](./docs/metrics.md) ### Examples - [Full GameServer Configuration](./examples/gameserver.yaml) diff --git a/build/Makefile b/build/Makefile index fdc208b126..24462a6891 100644 --- a/build/Makefile +++ b/build/Makefile @@ -348,7 +348,7 @@ push-build-image: # port forward the agones controller. # useful for pprof and stats viewing, etc -controller-portforward: PORT ?= 6060 +controller-portforward: PORT ?= 8080 controller-portforward: docker run --rm -it $(common_mounts) $(DOCKER_RUN_ARGS) \ -e "KUBECONFIG=/root/.kube/$(kubeconfig_file)" -p $(PORT):$(PORT) $(build_tag) \ @@ -395,6 +395,7 @@ do-release: git push -u upstream release-$(RELEASE_VERSION) @echo "Now go make the $(RELEASE_VERSION) release on Github!" +setup-test-cluster: DOCKER_RUN_ARGS+=--network=host setup-test-cluster: $(ensure-build-image) $(DOCKER_RUN) kubectl apply -f $(mount_path)/build/helm.yaml $(DOCKER_RUN) helm init --wait --service-account helm @@ -570,7 +571,7 @@ kind-delete-cluster: kind delete cluster --name $(KIND_PROFILE) # start an interactive shell with kubectl configured to target the kind cluster - kind-shell: $(ensure-build-image) +kind-shell: $(ensure-build-image) $(MAKE) shell KUBECONFIG="$(shell kind get kubeconfig-path --name="$(KIND_PROFILE)")" \ DOCKER_RUN_ARGS="--network=host $(DOCKER_RUN_ARGS)" diff --git a/build/helm.yaml b/build/helm.yaml index 33e689addd..2f2c6f4151 100644 --- a/build/helm.yaml +++ b/build/helm.yaml @@ -31,4 +31,23 @@ roleRef: subjects: - kind: ServiceAccount name: helm - namespace: kube-system \ No newline at end of file + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: cluster-admin + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" +rules: +- apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' +- nonResourceURLs: + - '*' + verbs: + - '*' \ No newline at end of file diff --git a/cmd/controller/main.go b/cmd/controller/main.go index be83cb7f38..fc4be9bbc5 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -32,21 +32,24 @@ import ( "agones.dev/agones/pkg/fleets" "agones.dev/agones/pkg/gameservers" "agones.dev/agones/pkg/gameserversets" + "agones.dev/agones/pkg/metrics" "agones.dev/agones/pkg/util/runtime" "agones.dev/agones/pkg/util/signals" "agones.dev/agones/pkg/util/webhooks" "github.com/heptiolabs/healthcheck" "github.com/pkg/errors" + prom "github.com/prometheus/client_golang/prometheus" "github.com/spf13/pflag" "github.com/spf13/viper" extclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" ) const ( + enableMetricsFlag = "metrics" sidecarImageFlag = "sidecar-image" sidecarCPURequestFlag = "sidecar-cpu-request" sidecarCPULimitFlag = "sidecar-cpu-limit" @@ -55,6 +58,7 @@ const ( maxPortFlag = "max-port" certFileFlag = "cert-file" keyFileFlag = "key-file" + kubeconfigFlag = "kubeconfig" workers = 2 defaultResync = 30 * time.Second ) @@ -73,7 +77,8 @@ func main() { logger.WithError(err).Fatal("Could not create controller from environment or flags") } - clientConf, err := rest.InClusterConfig() + // if the kubeconfig fails BuildConfigFromFlags will try in cluster config + clientConf, err := clientcmd.BuildConfigFromFlags("", ctlConf.KubeConfig) if err != nil { logger.WithError(err).Fatal("Could not create in cluster config") } @@ -93,11 +98,30 @@ func main() { logger.WithError(err).Fatal("Could not create the agones api clientset") } - health := healthcheck.NewHandler() wh := webhooks.NewWebHook(ctlConf.CertFile, ctlConf.KeyFile) agonesInformerFactory := externalversions.NewSharedInformerFactory(agonesClient, defaultResync) kubeInformationFactory := informers.NewSharedInformerFactory(kubeClient, defaultResync) + server := &httpServer{} + var health healthcheck.Handler + var metricsController *metrics.Controller + + if ctlConf.Metrics { + registry := prom.NewRegistry() + metricHandler, err := metrics.RegisterPrometheusExporter(registry) + if err != nil { + logger.WithError(err).Fatal("Could not create register prometheus exporter") + } + server.Handle("/metrics", metricHandler) + health = healthcheck.NewMetricsHandler(registry, "agones") + metricsController = metrics.NewController(kubeClient, agonesClient, agonesInformerFactory) + + } else { + health = healthcheck.NewHandler() + } + + server.Handle("/", health) + allocationMutex := &sync.Mutex{} gsController := gameservers.NewController(wh, health, allocationMutex, @@ -112,15 +136,19 @@ func main() { fasController := fleetautoscalers.NewController(wh, health, kubeClient, extClient, agonesClient, agonesInformerFactory) + rs := []runner{ + wh, gsController, gsSetController, fleetController, faController, fasController, metricsController, server, + } + stop := signals.NewStopChannel() kubeInformationFactory.Start(stop) agonesInformerFactory.Start(stop) - rs := []runner{ - wh, gsController, gsSetController, fleetController, faController, fasController, healthServer{handler: health}, - } for _, r := range rs { + if r == nil { + continue + } go func(rr runner) { if runErr := rr.Run(workers, stop); runErr != nil { logger.WithError(runErr).Fatalf("could not start runner: %s", reflect.TypeOf(rr)) @@ -145,6 +173,7 @@ func parseEnvFlags() config { viper.SetDefault(pullSidecarFlag, false) viper.SetDefault(certFileFlag, filepath.Join(base, "certs/server.crt")) viper.SetDefault(keyFileFlag, filepath.Join(base, "certs/server.key")) + viper.SetDefault(enableMetricsFlag, true) pflag.String(sidecarImageFlag, viper.GetString(sidecarImageFlag), "Flag to overwrite the GameServer sidecar image that is used. Can also use SIDECAR env variable") pflag.String(sidecarCPULimitFlag, viper.GetString(sidecarCPULimitFlag), "Flag to overwrite the GameServer sidecar container's cpu limit. Can also use SIDECAR_CPU_LIMIT env variable") @@ -154,6 +183,8 @@ func parseEnvFlags() config { pflag.Int32(maxPortFlag, 0, "Required. The maximum port that that a GameServer can be allocated to. Can also use MAX_PORT env variable") pflag.String(keyFileFlag, viper.GetString(keyFileFlag), "Optional. Path to the key file") pflag.String(certFileFlag, viper.GetString(certFileFlag), "Optional. Path to the crt file") + pflag.String(kubeconfigFlag, viper.GetString(kubeconfigFlag), "Optional. kubeconfig to run the controller out of the cluster. Only use it for debugging as webhook won't works.") + pflag.Bool(enableMetricsFlag, viper.GetBool(enableMetricsFlag), "Flag to activate metrics of Agones. Can also use METRICS env variable.") pflag.Parse() viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) @@ -165,6 +196,8 @@ func parseEnvFlags() config { runtime.Must(viper.BindEnv(maxPortFlag)) runtime.Must(viper.BindEnv(keyFileFlag)) runtime.Must(viper.BindEnv(certFileFlag)) + runtime.Must(viper.BindEnv(kubeconfigFlag)) + runtime.Must(viper.BindEnv(enableMetricsFlag)) runtime.Must(viper.BindPFlags(pflag.CommandLine)) request, err := resource.ParseQuantity(viper.GetString(sidecarCPURequestFlag)) @@ -186,6 +219,8 @@ func parseEnvFlags() config { AlwaysPullSidecar: viper.GetBool(pullSidecarFlag), KeyFile: viper.GetString(keyFileFlag), CertFile: viper.GetString(certFileFlag), + KubeConfig: viper.GetString(kubeconfigFlag), + Metrics: viper.GetBool(enableMetricsFlag), } } @@ -197,8 +232,10 @@ type config struct { SidecarCPURequest resource.Quantity SidecarCPULimit resource.Quantity AlwaysPullSidecar bool + Metrics bool KeyFile string CertFile string + KubeConfig string } // validate ensures the ctlConfig data is valid. @@ -216,21 +253,21 @@ type runner interface { Run(workers int, stop <-chan struct{}) error } -type healthServer struct { - handler http.Handler +type httpServer struct { + http.ServeMux } -func (h healthServer) Run(workers int, stop <-chan struct{}) error { - logger.Info("Starting health check...") +func (h *httpServer) Run(workers int, stop <-chan struct{}) error { + logger.Info("Starting http server...") srv := &http.Server{ Addr: ":8080", - Handler: h.handler, + Handler: h, } defer srv.Close() // nolint: errcheck if err := srv.ListenAndServe(); err != nil { if err == http.ErrServerClosed { - logger.WithError(err).Info("health check: http server closed") + logger.WithError(err).Info("http server closed") } else { wrappedErr := errors.Wrap(err, "Could not listen on :8080") runtime.HandleError(logger.WithError(wrappedErr), wrappedErr) diff --git a/docs/metrics.md b/docs/metrics.md new file mode 100644 index 0000000000..af52dd6010 --- /dev/null +++ b/docs/metrics.md @@ -0,0 +1,78 @@ +# Metrics + +Agones controller exposes metrics via [OpenCensus](https://opencensus.io/). OpenCensus is a single distribution of libraries that collect metrics and distributed traces from your services, we only use it for metrics but it will allow us to support multiple exporters in the future. + +We choose to start with Prometheus as this is the most popular with Kubernetes but it is also compatible with Stackdriver. +If you need another exporter, check the [list of supported](https://opencensus.io/exporters/supported-exporters/go/) exporters. It should be pretty straightforward to register a new one.(Github PR are more than welcomed) + +We plan to support multiple exporters in the future via environement variables and helm flags. + +## Backend integrations + +### Prometheus + +If you are running a [Prometheus](https://prometheus.io/) intance you just need to ensure that metrics and kubernetes service discovery are enabled. (helm chart values `agones.metrics.enabled` and `agones.metrics.prometheusServiceDiscovery`). This will automatically add annotations required by Prometheus to discover Agones metrics and start collecting them. (see [example](https://github.com/prometheus/prometheus/tree/master/documentation/examples/kubernetes-rabbitmq)) + +### Prometheus Operator + +If you have [Prometheus operator](https://github.com/coreos/prometheus-operator) installed in your cluster, make sure to add a [`ServiceMonitor`](https://github.com/coreos/prometheus-operator/blob/v0.17.0/Documentation/api.md#servicemonitorspec) to discover Agones metrics as shown below: + +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: agones + labels: + app: agones +spec: + selector: + matchLabels: + stable.agones.dev/role: controller + endpoints: + - port: web +``` + +Finally include that `ServiceMonitor` in your [Prometheus instance CRD](https://github.com/coreos/prometheus-operator/blob/v0.17.0/Documentation/user-guides/getting-started.md#include-servicemonitors), this is usually done by adding a label to the `ServiceMonitor` above that is matched by the prometheus instance of your choice. + +### Stackdriver + +We don't yet support the [OpenCensus Stackdriver exporter](https://opencensus.io/exporters/supported-exporters/go/stackdriver/) but you can still use the Prometheus Stackdriver integration by following these [instructions](https://cloud.google.com/monitoring/kubernetes-engine/prometheus). +Annotations required by this integration can be activated by setting the `agones.metrics.prometheusServiceDiscovery` to true (default) via the [helm chart value](../install/helm/README.md#configuration). + +## Metrics available + +| Name | Description | Type | +|-------------------------------------------------|---------------------------------------------------------------------|---------| +| agones_gameservers_count | The number of gameservers per fleet and status | gauge | +| agones_fleet_allocations_count | The number of fleet allocations per fleet | gauge | +| agones_gameservers_total | The total of gameservers per fleet and status | counter | +| agones_fleet_allocations_total | The total of fleet allocations per fleet | counter | +| agones_fleets_replicas_count | The number of replicas per fleet (total, desired, ready, allocated) | gauge | +| agones_fleet_autoscalers_able_to_scale | The fleet autoscaler can access the fleet to scale | gauge | +| agones_fleet_autoscalers_buffer_limits | he limits of buffer based fleet autoscalers (min, max) | gauge | +| agones_fleet_autoscalers_buffer_size | The buffer size of fleet autoscalers (count or percentage) | gauge | +| agones_fleet_autoscalers_current_replicas_count | The current replicas count as seen by autoscalers | gauge | +| agones_fleet_autoscalers_desired_replicas_count | The desired replicas count as seen by autoscalers | gauge | +| agones_fleet_autoscalers_limited | The fleet autoscaler is capped (1) | gauge | + +## Dashboard + +Grafana and Stackdriver - Coming Soon + +## Adding more metrics + +If you want to contribute and add more metrics we recommend to use shared informers (cache) as it is currently implemented in the [metrics controller](../pkg/metrics/controller.go). Using shared informers allows to keep metrics code in one place and doesn't overload the Kubernetes API. + +However there is some cases where you will have to add code inside your ressource controller (eg. latency metrics), you should minize metrics code in your controller by adding specific functions in the metrics packages as shown below. + +```golang +package metrics + +import "go.opencensus.io/stats" + +... + +func RecordSomeLatency(latency int64,ressourceName string) { + stats.RecordWithTags(....) +} +``` diff --git a/install/helm/README.md b/install/helm/README.md index 54c8f10cff..71e2d55b6a 100644 --- a/install/helm/README.md +++ b/install/helm/README.md @@ -86,6 +86,8 @@ The following tables lists the configurable parameters of the Agones chart and t | `agones.rbacEnabled` | Creates RBAC resources. Must be set for any cluster configured with RBAC | `true` | | `agones.crds.install` | Install the CRDs with this chart. Useful to disable if you want to subchart (since crd-install hook is broken), so you can copy the CRDs into your own chart. | `true` | | `agones.crds.cleanupOnDelete` | Run the pre-delete hook to delete all GameServers and their backing Pods when deleting the helm chart, so that all CRDs can be removed on chart deletion | `true` | +| `agones.metrics.enabled` | Enables controller metrics on port `8080` and path `/metrics` | `true` | +| `agones.metrics.prometheusServiceDiscovery` | Adds annotations for Prometheus ServiceDiscovery (and also Strackdriver) | `true` | | `agones.serviceaccount.controller` | Service account name for the controller | `agones-controller` | | `agones.serviceaccount.sdk` | Service account name for the sdk | `agones-sdk` | | `agones.image.registry` | Global image registry for all images | `gcr.io/agones-images` | @@ -99,7 +101,7 @@ The following tables lists the configurable parameters of the Agones chart and t | `agones.image.sdk.alwaysPull` | Tells if the sdk image should always be pulled | `false` | | `agones.image.ping.name` | ( ⚠️ development feature ⚠️ ) Image name for the ping service | `agones-ping` | | `agones.image.ping.pullPolicy` | ( ⚠️ development feature ⚠️ ) Image pull policy for the ping service | `IfNotPresent` | -| `agones.controller.healthCheck.http.port` | Port to use for liveness probe service | `8080` | +| `agones.controller.http.port` | Port to use for liveness probe service and metrics | `8080` | | `agones.controller.healthCheck.initialDelaySeconds` | Initial delay before performing the first probe (in seconds) | `3` | | `agones.controller.healthCheck.periodSeconds` | Seconds between every liveness probe (in seconds) | `3` | | `agones.controller.healthCheck.failureThreshold` | Number of times before giving up (in seconds) | `3` | diff --git a/install/helm/agones/templates/controller.yaml b/install/helm/agones/templates/controller.yaml index 03acde420f..7a615d4390 100644 --- a/install/helm/agones/templates/controller.yaml +++ b/install/helm/agones/templates/controller.yaml @@ -39,6 +39,11 @@ spec: cluster-autoscaler.kubernetes.io/safe-to-evict: {{ .Values.agones.controller.safeToEvict | quote }} {{- if .Values.agones.controller.generateTLS }} revision/tls-cert: {{ .Release.Revision | quote }} +{{- end }} +{{- if and (.Values.agones.metrics.prometheusServiceDiscovery) (.Values.agones.metrics.enabled) }} + prometheus.io/scrape: "true" + prometheus.io/port: {{ .Values.agones.controller.http.port | quote }} + prometheus.io/path: "/metrics" {{- end }} labels: stable.agones.dev/role: controller @@ -64,12 +69,14 @@ spec: value: {{ .Values.agones.image.sdk.alwaysPull | quote }} - name: SIDECAR_CPU_REQUEST value: {{ .Values.agones.image.sdk.cpuRequest | quote }} + - name: METRICS + value: {{ .Values.agones.metrics.enabled | quote }} - name: SIDECAR_CPU_LIMIT value: {{ .Values.agones.image.sdk.cpuLimit | quote }} livenessProbe: httpGet: path: /live - port: {{ .Values.agones.controller.healthCheck.http.port }} + port: {{ .Values.agones.controller.http.port }} initialDelaySeconds: {{ .Values.agones.controller.healthCheck.initialDelaySeconds }} periodSeconds: {{ .Values.agones.controller.healthCheck.periodSeconds }} failureThreshold: {{ .Values.agones.controller.healthCheck.failureThreshold }} diff --git a/install/helm/agones/templates/service.yaml b/install/helm/agones/templates/service.yaml index 5636e78131..f52c4d9646 100644 --- a/install/helm/agones/templates/service.yaml +++ b/install/helm/agones/templates/service.yaml @@ -18,7 +18,7 @@ metadata: name: agones-controller-service namespace: {{ .Release.Namespace }} labels: - component: controller + stable.agones.dev/role: controller app: {{ template "agones.name" . }} chart: {{ template "agones.chart" . }} release: {{ .Release.Name }} @@ -27,5 +27,8 @@ spec: selector: stable.agones.dev/role: controller ports: - - port: 443 - targetPort: 8081 \ No newline at end of file + - name: webhooks + port: 443 + targetPort: 8081 + - name: web + port: {{ .Values.agones.controller.http.port }} \ No newline at end of file diff --git a/install/helm/agones/values.yaml b/install/helm/agones/values.yaml index f43421e144..ba14e202ab 100644 --- a/install/helm/agones/values.yaml +++ b/install/helm/agones/values.yaml @@ -15,6 +15,9 @@ # Declare variables to be passed into your templates. agones: + metrics: + enabled: true + prometheusServiceDiscovery: true rbacEnabled: true crds: install: true @@ -26,9 +29,9 @@ agones: resources: {} generateTLS: true safeToEvict: false + http: + port: 8080 healthCheck: - http: - port: 8080 initialDelaySeconds: 3 periodSeconds: 3 failureThreshold: 3 diff --git a/install/yaml/install.yaml b/install/yaml/install.yaml index f690457c5c..88e999f346 100644 --- a/install/yaml/install.yaml +++ b/install/yaml/install.yaml @@ -810,7 +810,7 @@ metadata: name: agones-controller-service namespace: agones-system labels: - component: controller + stable.agones.dev/role: controller app: agones chart: agones-0.7.0-rc release: agones-manual @@ -819,8 +819,11 @@ spec: selector: stable.agones.dev/role: controller ports: - - port: 443 + - name: webhooks + port: 443 targetPort: 8081 + - name: web + port: 8080 --- # Source: agones/templates/controller.yaml # Copyright 2018 Google Inc. All Rights Reserved. @@ -862,6 +865,9 @@ spec: metadata: annotations: cluster-autoscaler.kubernetes.io/safe-to-evict: "false" + prometheus.io/scrape: "true" + prometheus.io/port: "8080" + prometheus.io/path: "/metrics" labels: stable.agones.dev/role: controller app: agones @@ -886,6 +892,8 @@ spec: value: "false" - name: SIDECAR_CPU_REQUEST value: "30m" + - name: METRICS + value: "true" - name: SIDECAR_CPU_LIMIT value: "0" livenessProbe: diff --git a/pkg/metrics/controller.go b/pkg/metrics/controller.go new file mode 100644 index 0000000000..1ae7aaec70 --- /dev/null +++ b/pkg/metrics/controller.go @@ -0,0 +1,359 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "context" + "strconv" + "sync" + "time" + + stablev1alpha1 "agones.dev/agones/pkg/apis/stable/v1alpha1" + "agones.dev/agones/pkg/client/clientset/versioned" + "agones.dev/agones/pkg/client/informers/externalversions" + listerv1alpha1 "agones.dev/agones/pkg/client/listers/stable/v1alpha1" + "agones.dev/agones/pkg/util/runtime" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" +) + +var ( + // MetricResyncPeriod is the interval to re-synchronize metrics based on indexed cache. + MetricResyncPeriod = time.Second * 1 +) + +// Controller is a metrics controller +type Controller struct { + logger *logrus.Entry + gameServerLister listerv1alpha1.GameServerLister + faLister listerv1alpha1.FleetAllocationLister + gameServerSynced cache.InformerSynced + fleetSynced cache.InformerSynced + fasSynced cache.InformerSynced + faSynced cache.InformerSynced + lock sync.Mutex + gsCount GameServerCount + faCount map[string]int64 +} + +// NewController returns a new metrics controller +func NewController( + kubeClient kubernetes.Interface, + agonesClient versioned.Interface, + agonesInformerFactory externalversions.SharedInformerFactory) *Controller { + + gameServer := agonesInformerFactory.Stable().V1alpha1().GameServers() + gsInformer := gameServer.Informer() + + fa := agonesInformerFactory.Stable().V1alpha1().FleetAllocations() + faInformer := fa.Informer() + fleets := agonesInformerFactory.Stable().V1alpha1().Fleets() + fInformer := fleets.Informer() + fas := agonesInformerFactory.Stable().V1alpha1().FleetAutoscalers() + fasInformer := fas.Informer() + + c := &Controller{ + gameServerLister: gameServer.Lister(), + gameServerSynced: gsInformer.HasSynced, + faLister: fa.Lister(), + fleetSynced: fInformer.HasSynced, + fasSynced: fasInformer.HasSynced, + faSynced: faInformer.HasSynced, + gsCount: GameServerCount{}, + faCount: map[string]int64{}, + } + + c.logger = runtime.NewLoggerWithType(c) + + fInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: c.recordFleetChanges, + UpdateFunc: func(old, new interface{}) { + c.recordFleetChanges(new) + }, + DeleteFunc: c.recordFleetDeletion, + }) + + fasInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(added interface{}) { + c.recordFleetAutoScalerChanges(nil, added) + }, + UpdateFunc: c.recordFleetAutoScalerChanges, + DeleteFunc: c.recordFleetAutoScalerDeletion, + }) + + faInformer.AddEventHandlerWithResyncPeriod(cache.ResourceEventHandlerFuncs{ + UpdateFunc: c.recordFleetAllocationChanges, + }, 0) + + gsInformer.AddEventHandlerWithResyncPeriod(cache.ResourceEventHandlerFuncs{ + UpdateFunc: c.recordGameServerStatusChanges, + }, 0) + + c.registerViews() + return c +} + +// register all our views to OpenCensus +func (c *Controller) registerViews() { + for _, v := range views { + if err := view.Register(v); err != nil { + c.logger.WithError(err).Error("could not register view") + } + } +} + +// unregister views, this is only useful for tests as it trigger reporting. +func (c *Controller) unRegisterViews() { + for _, v := range views { + view.Unregister(v) + } +} + +func (c *Controller) recordFleetAutoScalerChanges(old, new interface{}) { + + fas, ok := new.(*stablev1alpha1.FleetAutoscaler) + if !ok { + return + } + + // we looking for fleet name changes if that happens we need to reset + // metrics for the old fas. + if old != nil { + if oldFas, ok := old.(*stablev1alpha1.FleetAutoscaler); ok && + oldFas.Spec.FleetName != fas.Spec.FleetName { + c.recordFleetAutoScalerDeletion(old) + } + } + + // fleet autoscaler has been deleted last value should be 0 + if fas.DeletionTimestamp != nil { + c.recordFleetAutoScalerDeletion(fas) + return + } + + ctx, _ := tag.New(context.Background(), tag.Upsert(keyName, fas.Name), + tag.Upsert(keyFleetName, fas.Spec.FleetName)) + + ableToScale := 0 + limited := 0 + if fas.Status.AbleToScale { + ableToScale = 1 + } + if fas.Status.ScalingLimited { + limited = 1 + } + // recording status + stats.Record(ctx, + fasCurrentReplicasStats.M(int64(fas.Status.CurrentReplicas)), + fasDesiredReplicasStats.M(int64(fas.Status.DesiredReplicas)), + fasAbleToScaleStats.M(int64(ableToScale)), + fasLimitedStats.M(int64(limited))) + + // recording buffer policy + if fas.Spec.Policy.Buffer != nil { + // recording limits + c.recordWithTags(ctx, []tag.Mutator{tag.Upsert(keyType, "max")}, + fasBufferLimitsCountStats.M(int64(fas.Spec.Policy.Buffer.MaxReplicas))) + c.recordWithTags(ctx, []tag.Mutator{tag.Upsert(keyType, "min")}, + fasBufferLimitsCountStats.M(int64(fas.Spec.Policy.Buffer.MinReplicas))) + + // recording size + if fas.Spec.Policy.Buffer.BufferSize.Type == intstr.String { + // as percentage + sizeString := fas.Spec.Policy.Buffer.BufferSize.StrVal + if sizeString != "" { + if size, err := strconv.Atoi(sizeString[:len(sizeString)-1]); err == nil { + c.recordWithTags(ctx, []tag.Mutator{tag.Upsert(keyType, "percentage")}, + fasBufferSizeStats.M(int64(size))) + } + } + } else { + // as count + c.recordWithTags(ctx, []tag.Mutator{tag.Upsert(keyType, "count")}, + fasBufferSizeStats.M(int64(fas.Spec.Policy.Buffer.BufferSize.IntVal))) + } + } +} + +func (c *Controller) recordFleetAutoScalerDeletion(obj interface{}) { + fas, ok := obj.(*stablev1alpha1.FleetAutoscaler) + if !ok { + return + } + ctx, _ := tag.New(context.Background(), tag.Upsert(keyName, fas.Name), + tag.Upsert(keyFleetName, fas.Spec.FleetName)) + + // recording status + stats.Record(ctx, + fasCurrentReplicasStats.M(int64(0)), + fasDesiredReplicasStats.M(int64(0)), + fasAbleToScaleStats.M(int64(0)), + fasLimitedStats.M(int64(0))) +} + +func (c *Controller) recordFleetChanges(obj interface{}) { + f, ok := obj.(*stablev1alpha1.Fleet) + if !ok { + return + } + + // fleet has been deleted last value should be 0 + if f.DeletionTimestamp != nil { + c.recordFleetDeletion(f) + return + } + + c.recordFleetReplicas(f.Name, f.Status.Replicas, f.Status.AllocatedReplicas, + f.Status.ReadyReplicas, f.Spec.Replicas) +} + +func (c *Controller) recordFleetDeletion(obj interface{}) { + f, ok := obj.(*stablev1alpha1.Fleet) + if !ok { + return + } + + c.recordFleetReplicas(f.Name, 0, 0, 0, 0) +} + +func (c *Controller) recordFleetReplicas(fleetName string, total, allocated, ready, desired int32) { + + ctx, _ := tag.New(context.Background(), tag.Upsert(keyName, fleetName)) + + c.recordWithTags(ctx, []tag.Mutator{tag.Upsert(keyType, "total")}, + fleetsReplicasCountStats.M(int64(total))) + c.recordWithTags(ctx, []tag.Mutator{tag.Upsert(keyType, "allocated")}, + fleetsReplicasCountStats.M(int64(allocated))) + c.recordWithTags(ctx, []tag.Mutator{tag.Upsert(keyType, "ready")}, + fleetsReplicasCountStats.M(int64(ready))) + c.recordWithTags(ctx, []tag.Mutator{tag.Upsert(keyType, "desired")}, + fleetsReplicasCountStats.M(int64(desired))) +} + +func (c *Controller) recordWithTags(ctx context.Context, mutators []tag.Mutator, ms ...stats.Measurement) { + if err := stats.RecordWithTags(ctx, mutators, ms...); err != nil { + c.logger.WithError(err).Warn("error while recoding stats") + } +} + +// recordGameServerStatusChanged records gameserver status changes, however since it's based +// on cache events some events might collapsed and not appear, for example transition state +// like creating, port allocation, could be skipped. +// This is still very useful for final state, like READY, ERROR and since this is a counter +// (as opposed to gauge) you can aggregate using a rate, let's say how many gameserver are failing +// per second. +// Addition to the cache are not handled, otherwise resync would make metrics inaccurate by doubling +// current gameservers states. +func (c *Controller) recordGameServerStatusChanges(old, new interface{}) { + newGs, ok := new.(*stablev1alpha1.GameServer) + if !ok { + return + } + oldGs, ok := old.(*stablev1alpha1.GameServer) + if !ok { + return + } + if newGs.Status.State != oldGs.Status.State { + fleetName := newGs.Labels[stablev1alpha1.FleetNameLabel] + if fleetName == "" { + fleetName = "none" + } + c.recordWithTags(context.Background(), []tag.Mutator{tag.Upsert(keyType, string(newGs.Status.State)), + tag.Upsert(keyFleetName, fleetName)}, gameServerTotalStats.M(1)) + } +} + +// record fleet allocations total by watching cache changes. +func (c *Controller) recordFleetAllocationChanges(old, new interface{}) { + newFa, ok := new.(*stablev1alpha1.FleetAllocation) + if !ok { + return + } + oldFa, ok := old.(*stablev1alpha1.FleetAllocation) + if !ok { + return + } + // fleet allocations are added without gameserver allocated + // but then get modified on successful allocation with their gameserver + if oldFa.Status.GameServer == nil && newFa.Status.GameServer != nil { + c.recordWithTags(context.Background(), []tag.Mutator{tag.Upsert(keyFleetName, newFa.Spec.FleetName)}, + fleetAllocationTotalStats.M(1)) + } +} + +// Run the Metrics controller. Will block until stop is closed. +// Collect metrics via cache changes and parse the cache periodically to record resource counts. +func (c *Controller) Run(workers int, stop <-chan struct{}) error { + c.logger.Info("Wait for cache sync") + if !cache.WaitForCacheSync(stop, c.gameServerSynced, c.fleetSynced, + c.fasSynced, c.faSynced) { + return errors.New("failed to wait for caches to sync") + } + wait.Until(c.collect, MetricResyncPeriod, stop) + return nil +} + +// collect all metrics that are not event-based. +// this is fired periodically. +func (c *Controller) collect() { + c.lock.Lock() + defer c.lock.Unlock() + c.collectGameServerCounts() + c.collectFleetAllocationCounts() +} + +// collects fleet allocations count by going through our informer cache +func (c *Controller) collectFleetAllocationCounts() { + //reset fleet allocations count per fleet name + for fleetName := range c.faCount { + c.faCount[fleetName] = 0 + } + + fleetAllocations, err := c.faLister.List(labels.Everything()) + if err != nil { + c.logger.WithError(err).Warn("failed listing fleet allocations") + } + + for _, fa := range fleetAllocations { + c.faCount[fa.Spec.FleetName]++ + } + + for fleetName, count := range c.faCount { + c.recordWithTags(context.Background(), []tag.Mutator{tag.Insert(keyFleetName, fleetName)}, + fleetAllocationCountStats.M(count)) + } +} + +// collects gameservers count by going through our informer cache +// this not meant to be called concurrently +func (c *Controller) collectGameServerCounts() { + + gameservers, err := c.gameServerLister.List(labels.Everything()) + if err != nil { + c.logger.WithError(err).Warn("failed listing gameservers") + } + + if err := c.gsCount.record(gameservers); err != nil { + c.logger.WithError(err).Warn("error while recoding stats") + } +} diff --git a/pkg/metrics/controller_test.go b/pkg/metrics/controller_test.go new file mode 100644 index 0000000000..d33038c71a --- /dev/null +++ b/pkg/metrics/controller_test.go @@ -0,0 +1,225 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "strings" + "testing" + + "agones.dev/agones/pkg/apis/stable/v1alpha1" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestControllerGameServerCount(t *testing.T) { + + registry := prometheus.NewRegistry() + _, err := RegisterPrometheusExporter(registry) + assert.Nil(t, err) + + c := newFakeController() + defer c.close() + + gs1 := gameServer("test-fleet", v1alpha1.Creating) + c.gsWatch.Add(gs1) + gs1 = gs1.DeepCopy() + gs1.Status.State = v1alpha1.Ready + c.gsWatch.Modify(gs1) + + c.sync() + c.collect() + c.report() + + gs1 = gs1.DeepCopy() + gs1.Status.State = v1alpha1.Shutdown + c.gsWatch.Modify(gs1) + c.gsWatch.Add(gameServer("", v1alpha1.PortAllocation)) + c.gsWatch.Add(gameServer("", v1alpha1.PortAllocation)) + + c.sync() + c.collect() + c.report() + + assert.Nil(t, testutil.GatherAndCompare(registry, strings.NewReader(gsCountExpected), "agones_gameservers_count")) +} + +func TestControllerFleetAllocationCount(t *testing.T) { + + registry := prometheus.NewRegistry() + _, err := RegisterPrometheusExporter(registry) + assert.Nil(t, err) + + c := newFakeController() + defer c.close() + + fa1 := fleetAllocation("deleted-fleet") + c.faWatch.Add(fa1) + c.faWatch.Add(fleetAllocation("test-fleet")) + c.faWatch.Add(fleetAllocation("test-fleet")) + c.faWatch.Add(fleetAllocation("test-fleet2")) + + c.sync() + c.collect() + c.report() + + c.faWatch.Delete(fa1) + c.faWatch.Add(fleetAllocation("test-fleet")) + c.faWatch.Add(fleetAllocation("test-fleet2")) + + c.sync() + c.collect() + c.report() + + assert.Nil(t, testutil.GatherAndCompare(registry, strings.NewReader(faCountExpected), "agones_fleet_allocations_count")) +} + +func TestControllerFleetAllocationTotal(t *testing.T) { + + registry := prometheus.NewRegistry() + _, err := RegisterPrometheusExporter(registry) + assert.Nil(t, err) + + c := newFakeController() + defer c.close() + c.run(t) + // non allocated should not be counted + fa1 := fleetAllocation("unallocated") + fa1.Status.GameServer = nil + c.faWatch.Add(fa1) + c.faWatch.Delete(fa1) + + for i := 0; i < 3; i++ { + fa := fleetAllocation("test") + // only fleet allocation that were not allocated to a gameserver are collected + // this way we avoid counting multiple update events. + fa.Status.GameServer = nil + c.faWatch.Add(fa) + faUpdated := fa.DeepCopy() + faUpdated.Status.GameServer = gameServer("test", v1alpha1.Allocated) + c.faWatch.Modify(faUpdated) + // make sure we count only one event + c.faWatch.Modify(faUpdated) + } + for i := 0; i < 2; i++ { + fa := fleetAllocation("test2") + fa.Status.GameServer = nil + c.faWatch.Add(fa) + faUpdated := fa.DeepCopy() + faUpdated.Status.GameServer = gameServer("test2", v1alpha1.Allocated) + c.faWatch.Modify(faUpdated) + } + c.sync() + c.report() + + assert.Nil(t, testutil.GatherAndCompare(registry, strings.NewReader(faTotalExpected), "agones_fleet_allocations_total")) +} + +func TestControllerGameServersTotal(t *testing.T) { + + registry := prometheus.NewRegistry() + _, err := RegisterPrometheusExporter(registry) + assert.Nil(t, err) + + c := newFakeController() + defer c.close() + c.run(t) + + // deleted gs should not be counted + gs := gameServer("deleted", v1alpha1.Creating) + c.gsWatch.Add(gs) + c.gsWatch.Delete(gs) + + generateGsEvents(16, v1alpha1.Creating, "test", c.gsWatch) + generateGsEvents(15, v1alpha1.Scheduled, "test", c.gsWatch) + generateGsEvents(10, v1alpha1.Starting, "test", c.gsWatch) + generateGsEvents(1, v1alpha1.Unhealthy, "test", c.gsWatch) + generateGsEvents(19, v1alpha1.Creating, "", c.gsWatch) + generateGsEvents(18, v1alpha1.Scheduled, "", c.gsWatch) + generateGsEvents(16, v1alpha1.Starting, "", c.gsWatch) + generateGsEvents(1, v1alpha1.Unhealthy, "", c.gsWatch) + + c.sync() + c.report() + + assert.Nil(t, testutil.GatherAndCompare(registry, strings.NewReader(gsTotalExpected), "agones_gameservers_total")) +} + +func TestControllerFleetReplicasCount(t *testing.T) { + + registry := prometheus.NewRegistry() + _, err := RegisterPrometheusExporter(registry) + assert.Nil(t, err) + + c := newFakeController() + defer c.close() + c.run(t) + + f := fleet("fleet-test", 8, 2, 5, 1) + fd := fleet("fleet-deleted", 100, 100, 100, 100) + c.fleetWatch.Add(f) + f = f.DeepCopy() + f.Status.ReadyReplicas = 1 + f.Spec.Replicas = 5 + c.fleetWatch.Modify(f) + c.fleetWatch.Add(fd) + c.fleetWatch.Delete(fd) + + c.sync() + c.report() + + assert.Nil(t, testutil.GatherAndCompare(registry, strings.NewReader(fleetReplicasCountExpected), "agones_fleets_replicas_count")) +} + +func TestControllerFleetAutoScalerState(t *testing.T) { + registry := prometheus.NewRegistry() + _, err := RegisterPrometheusExporter(registry) + assert.Nil(t, err) + + c := newFakeController() + defer c.close() + c.run(t) + + // testing fleet name change + fasFleetNameChange := fleetAutoScaler("first-fleet", "name-switch") + c.fasWatch.Add(fasFleetNameChange) + fasFleetNameChange = fasFleetNameChange.DeepCopy() + fasFleetNameChange.Spec.Policy.Buffer.BufferSize = intstr.FromInt(10) + fasFleetNameChange.Spec.Policy.Buffer.MaxReplicas = 50 + fasFleetNameChange.Spec.Policy.Buffer.MinReplicas = 10 + fasFleetNameChange.Status.CurrentReplicas = 20 + fasFleetNameChange.Status.DesiredReplicas = 10 + fasFleetNameChange.Status.ScalingLimited = true + c.fasWatch.Modify(fasFleetNameChange) + fasFleetNameChange = fasFleetNameChange.DeepCopy() + fasFleetNameChange.Spec.FleetName = "second-fleet" + c.fasWatch.Modify(fasFleetNameChange) + // testing deletion + fasDeleted := fleetAutoScaler("deleted-fleet", "deleted") + fasDeleted.Spec.Policy.Buffer.BufferSize = intstr.FromString("50%") + fasDeleted.Spec.Policy.Buffer.MaxReplicas = 150 + fasDeleted.Spec.Policy.Buffer.MinReplicas = 15 + c.fasWatch.Add(fasDeleted) + c.fasWatch.Delete(fasDeleted) + + c.sync() + c.report() + + assert.Nil(t, testutil.GatherAndCompare(registry, strings.NewReader(fasStateExpected), + "agones_fleet_autoscalers_able_to_scale", "agones_fleet_autoscalers_buffer_limits", "agones_fleet_autoscalers_buffer_size", + "agones_fleet_autoscalers_current_replicas_count", "agones_fleet_autoscalers_desired_replicas_count", "agones_fleet_autoscalers_limited")) + +} diff --git a/pkg/metrics/doc.go b/pkg/metrics/doc.go new file mode 100644 index 0000000000..bf4b0708e3 --- /dev/null +++ b/pkg/metrics/doc.go @@ -0,0 +1,16 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package metrics records stats of agones controllers +package metrics diff --git a/pkg/metrics/exporter.go b/pkg/metrics/exporter.go new file mode 100644 index 0000000000..acaa6ccfef --- /dev/null +++ b/pkg/metrics/exporter.go @@ -0,0 +1,47 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "net/http" + "time" + + prom "github.com/prometheus/client_golang/prometheus" + "go.opencensus.io/exporter/prometheus" + "go.opencensus.io/stats/view" +) + +// RegisterPrometheusExporter register a prometheus exporter to OpenCensus with a given prometheus metric registry. +// It will automatically add go runtime and process metrics using default prometheus collectors. +// The function return an http.handler that you can use to expose the prometheus endpoint. +func RegisterPrometheusExporter(registry *prom.Registry) (http.Handler, error) { + pe, err := prometheus.NewExporter(prometheus.Options{ + Namespace: "agones", + Registry: registry, + }) + if err != nil { + return nil, err + } + if err := registry.Register(prom.NewProcessCollector(prom.ProcessCollectorOpts{})); err != nil { + return nil, err + } + if err := registry.Register(prom.NewGoCollector()); err != nil { + return nil, err + } + view.RegisterExporter(pe) + // since we're using prometheus we can report faster as we're only exposing metrics in memory + view.SetReportingPeriod(1 * time.Second) + return pe, nil +} diff --git a/pkg/metrics/gameservers.go b/pkg/metrics/gameservers.go new file mode 100644 index 0000000000..b12dde5b62 --- /dev/null +++ b/pkg/metrics/gameservers.go @@ -0,0 +1,72 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "context" + + stablev1alpha1 "agones.dev/agones/pkg/apis/stable/v1alpha1" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "k8s.io/apimachinery/pkg/util/errors" +) + +// GameServerCount is the count of gameserver per current state and per fleet name +type GameServerCount map[stablev1alpha1.State]map[string]int64 + +// increment adds the count of gameservers for a given fleetName and state +func (c GameServerCount) increment(fleetName string, state stablev1alpha1.State) { + fleets, ok := c[state] + if !ok { + fleets = map[string]int64{} + c[state] = fleets + } + fleets[fleetName]++ +} + +// reset sets zero to the whole metrics set +func (c GameServerCount) reset() { + for _, fleets := range c { + for fleet := range fleets { + fleets[fleet] = 0 + } + } +} + +// record counts the list of gameserver per status and fleet name and record it to OpenCensus +func (c GameServerCount) record(gameservers []*stablev1alpha1.GameServer) error { + // Currently there is no way to remove a metric so we have to reset our values to zero + // so that statuses that have no count anymore are zeroed. + // Otherwise OpenCensus will write the last value recorded to the prom endpoint. + // TL;DR we can't remove a gauge + c.reset() + // counts gameserver per state and fleet + for _, g := range gameservers { + c.increment(g.Labels[stablev1alpha1.FleetNameLabel], g.Status.State) + } + errs := []error{} + for state, fleets := range c { + for fleet, count := range fleets { + if fleet == "" { + fleet = "none" + } + if err := stats.RecordWithTags(context.Background(), []tag.Mutator{tag.Upsert(keyType, string(state)), + tag.Upsert(keyFleetName, fleet)}, gameServerCountStats.M(count)); err != nil { + errs = append(errs, err) + } + } + } + return errors.NewAggregate(errs) +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go new file mode 100644 index 0000000000..0cfa1152ff --- /dev/null +++ b/pkg/metrics/metrics.go @@ -0,0 +1,138 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +var ( + keyName = mustTagKey("name") + keyFleetName = mustTagKey("fleet_name") + keyType = mustTagKey("type") + + fleetsReplicasCountStats = stats.Int64("fleets/replicas_count", "The count of replicas per fleet", "1") + fleetsReplicasCountView = &view.View{ + Name: "fleets_replicas_count", + Measure: fleetsReplicasCountStats, + Description: "The number of replicas per fleet", + Aggregation: view.LastValue(), + TagKeys: []tag.Key{keyName, keyType}, + } + + fasBufferLimitsCountStats = stats.Int64("fas/buffer_limits", "The buffer limits of autoscalers", "1") + fasBufferLimitsCountView = &view.View{ + Name: "fleet_autoscalers_buffer_limits", + Measure: fasBufferLimitsCountStats, + Description: "The limits of buffer based fleet autoscalers", + Aggregation: view.LastValue(), + TagKeys: []tag.Key{keyName, keyType, keyFleetName}, + } + + fasBufferSizeStats = stats.Int64("fas/buffer_size", "The buffer size value of autoscalers", "1") + fasBufferSizeView = &view.View{ + Name: "fleet_autoscalers_buffer_size", + Measure: fasBufferSizeStats, + Description: "The buffer size of fleet autoscalers", + Aggregation: view.LastValue(), + TagKeys: []tag.Key{keyName, keyType, keyFleetName}, + } + + fasCurrentReplicasStats = stats.Int64("fas/current_replicas_count", "The current replicas cout as seen by autoscalers", "1") + fasCurrentReplicasView = &view.View{ + Name: "fleet_autoscalers_current_replicas_count", + Measure: fasCurrentReplicasStats, + Description: "The current replicas count as seen by autoscalers", + Aggregation: view.LastValue(), + TagKeys: []tag.Key{keyName, keyFleetName}, + } + + fasDesiredReplicasStats = stats.Int64("fas/desired_replicas_count", "The desired replicas cout as seen by autoscalers", "1") + fasDesiredReplicasView = &view.View{ + Name: "fleet_autoscalers_desired_replicas_count", + Measure: fasDesiredReplicasStats, + Description: "The desired replicas count as seen by autoscalers", + Aggregation: view.LastValue(), + TagKeys: []tag.Key{keyName, keyFleetName}, + } + + fasAbleToScaleStats = stats.Int64("fas/able_to_scale", "The fleet autoscaler can access the fleet to scale (0 indicates false, 1 indicates true)", "1") + fasAbleToScaleView = &view.View{ + Name: "fleet_autoscalers_able_to_scale", + Measure: fasAbleToScaleStats, + Description: "The fleet autoscaler can access the fleet to scale", + Aggregation: view.LastValue(), + TagKeys: []tag.Key{keyName, keyFleetName}, + } + + fasLimitedStats = stats.Int64("fas/limited", "The fleet autoscaler is capped (0 indicates false, 1 indicates true)", "1") + fasLimitedView = &view.View{ + Name: "fleet_autoscalers_limited", + Measure: fasLimitedStats, + Description: "The fleet autoscaler is capped", + Aggregation: view.LastValue(), + TagKeys: []tag.Key{keyName, keyFleetName}, + } + + gameServerCountStats = stats.Int64("gameservers/count", "The count of gameservers", "1") + gameServersCountView = &view.View{ + Name: "gameservers_count", + Measure: gameServerCountStats, + Description: "The number of gameservers", + Aggregation: view.LastValue(), + TagKeys: []tag.Key{keyType, keyFleetName}, + } + + fleetAllocationCountStats = stats.Int64("fleet_allocations/count", "The count of fleet allocations", "1") + fleetAllocationCountView = &view.View{ + Name: "fleet_allocations_count", + Measure: fleetAllocationCountStats, + Description: "The number of fleet allocations", + Aggregation: view.LastValue(), + TagKeys: []tag.Key{keyFleetName}, + } + + fleetAllocationTotalStats = stats.Int64("fleet_allocations/total", "The total of fleet allocations", "1") + fleetAllocationTotalView = &view.View{ + Name: "fleet_allocations_total", + Measure: fleetAllocationTotalStats, + Description: "The total of fleet allocations", + Aggregation: view.Count(), + TagKeys: []tag.Key{keyFleetName}, + } + + gameServerTotalStats = stats.Int64("gameservers/total", "The total of gameservers", "1") + gameServersTotalView = &view.View{ + Name: "gameservers_total", + Measure: gameServerTotalStats, + Description: "The total of gameservers", + Aggregation: view.Count(), + TagKeys: []tag.Key{keyType, keyFleetName}, + } + + views = []*view.View{fleetsReplicasCountView, gameServersCountView, gameServersTotalView, + fasBufferSizeView, fasBufferLimitsCountView, fasCurrentReplicasView, fasDesiredReplicasView, + fasAbleToScaleView, fasLimitedView, fleetAllocationCountView, fleetAllocationTotalView} +) + +func mustTagKey(key string) tag.Key { + t, err := tag.NewKey(key) + if err != nil { + panic(err) + } + return t +} diff --git a/pkg/metrics/util_test.go b/pkg/metrics/util_test.go new file mode 100644 index 0000000000..ba964cdaab --- /dev/null +++ b/pkg/metrics/util_test.go @@ -0,0 +1,268 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "context" + "testing" + + "agones.dev/agones/pkg/apis/stable/v1alpha1" + agtesting "agones.dev/agones/pkg/testing" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/apimachinery/pkg/util/uuid" + "k8s.io/apimachinery/pkg/watch" + k8stesting "k8s.io/client-go/testing" + "k8s.io/client-go/tools/cache" +) + +// newFakeController returns a controller, backed by the fake Clientset +func newFakeController() *fakeController { + m := agtesting.NewMocks() + c := NewController(m.KubeClient, m.AgonesClient, m.AgonesInformerFactory) + gsWatch := watch.NewFake() + faWatch := watch.NewFake() + fasWatch := watch.NewFake() + fleetWatch := watch.NewFake() + + m.AgonesClient.AddWatchReactor("gameservers", k8stesting.DefaultWatchReactor(gsWatch, nil)) + m.AgonesClient.AddWatchReactor("fleetallocations", k8stesting.DefaultWatchReactor(faWatch, nil)) + m.AgonesClient.AddWatchReactor("fleetautoscalers", k8stesting.DefaultWatchReactor(fasWatch, nil)) + m.AgonesClient.AddWatchReactor("fleets", k8stesting.DefaultWatchReactor(fleetWatch, nil)) + + stop, cancel := agtesting.StartInformers(m, c.gameServerSynced, c.faSynced, + c.fleetSynced, c.fasSynced) + + return &fakeController{ + Controller: c, + Mocks: m, + gsWatch: gsWatch, + faWatch: faWatch, + fasWatch: fasWatch, + fleetWatch: fleetWatch, + cancel: cancel, + stop: stop, + } +} + +func (c *fakeController) close() { + if c.cancel != nil { + c.cancel() + } +} + +func (c *fakeController) report() { + // hacky: unregistering views force view collections + // so to not have to wait for the reporting period to hit we can + // unregister and register again + c.unRegisterViews() + c.registerViews() +} + +func (c *fakeController) run(t *testing.T) { + go func() { + err := c.Controller.Run(1, c.stop) + assert.Nil(t, err) + }() + c.sync() +} + +func (c *fakeController) sync() { + cache.WaitForCacheSync(c.stop, c.gameServerSynced, c.fleetSynced, + c.fasSynced, c.faSynced) +} + +type fakeController struct { + *Controller + agtesting.Mocks + gsWatch *watch.FakeWatcher + faWatch *watch.FakeWatcher + fasWatch *watch.FakeWatcher + fleetWatch *watch.FakeWatcher + stop <-chan struct{} + cancel context.CancelFunc +} + +func gameServer(fleetName string, state v1alpha1.State) *v1alpha1.GameServer { + lbs := map[string]string{} + if fleetName != "" { + lbs[v1alpha1.FleetNameLabel] = fleetName + } + gs := &v1alpha1.GameServer{ + ObjectMeta: metav1.ObjectMeta{ + Name: rand.String(10), + Namespace: "default", + UID: uuid.NewUUID(), + Labels: lbs, + }, + Status: v1alpha1.GameServerStatus{ + State: state, + }, + } + return gs +} + +func generateGsEvents(count int, state v1alpha1.State, fleetName string, fakew *watch.FakeWatcher) { + for i := 0; i < count; i++ { + gs := gameServer(fleetName, v1alpha1.State("")) + fakew.Add(gs) + gsUpdated := gs.DeepCopy() + gsUpdated.Status.State = state + fakew.Modify(gsUpdated) + // make sure we count only one event + fakew.Modify(gsUpdated) + } +} + +func fleetAllocation(fleetName string) *v1alpha1.FleetAllocation { + return &v1alpha1.FleetAllocation{ + ObjectMeta: metav1.ObjectMeta{ + Name: rand.String(10), + Namespace: "default", + UID: uuid.NewUUID(), + }, + Spec: v1alpha1.FleetAllocationSpec{ + FleetName: fleetName, + }, + Status: v1alpha1.FleetAllocationStatus{ + GameServer: gameServer(fleetName, v1alpha1.Allocated), + }, + } +} + +func fleet(fleetName string, total, allocated, ready, desired int32) *v1alpha1.Fleet { + return &v1alpha1.Fleet{ + ObjectMeta: metav1.ObjectMeta{ + Name: fleetName, + Namespace: "default", + UID: uuid.NewUUID(), + }, + Spec: v1alpha1.FleetSpec{ + Replicas: desired, + }, + Status: v1alpha1.FleetStatus{ + AllocatedReplicas: allocated, + ReadyReplicas: ready, + Replicas: total, + }, + } +} + +func fleetAutoScaler(fleetName string, fasName string) *v1alpha1.FleetAutoscaler { + return &v1alpha1.FleetAutoscaler{ + ObjectMeta: metav1.ObjectMeta{ + Name: fasName, + Namespace: "default", + UID: uuid.NewUUID(), + }, + Spec: v1alpha1.FleetAutoscalerSpec{ + FleetName: fleetName, + Policy: v1alpha1.FleetAutoscalerPolicy{ + Type: v1alpha1.BufferPolicyType, + Buffer: &v1alpha1.BufferPolicy{ + MaxReplicas: 30, + MinReplicas: 10, + BufferSize: intstr.FromInt(11), + }, + }, + }, + Status: v1alpha1.FleetAutoscalerStatus{ + AbleToScale: true, + ScalingLimited: false, + CurrentReplicas: 10, + DesiredReplicas: 20, + }, + } +} + +var gsCountExpected = `# HELP agones_gameservers_count The number of gameservers +# TYPE agones_gameservers_count gauge +agones_gameservers_count{fleet_name="test-fleet",type="Ready"} 0 +agones_gameservers_count{fleet_name="test-fleet",type="Shutdown"} 1 +agones_gameservers_count{fleet_name="none",type="PortAllocation"} 2 +` +var faCountExpected = `# HELP agones_fleet_allocations_count The number of fleet allocations +# TYPE agones_fleet_allocations_count gauge +agones_fleet_allocations_count{fleet_name="test-fleet"} 3 +agones_fleet_allocations_count{fleet_name="test-fleet2"} 2 +agones_fleet_allocations_count{fleet_name="deleted-fleet"} 0 +` + +var faTotalExpected = `# HELP agones_fleet_allocations_total The total of fleet allocations +# TYPE agones_fleet_allocations_total counter +agones_fleet_allocations_total{fleet_name="test2"} 2 +agones_fleet_allocations_total{fleet_name="test"} 3 +` + +var gsTotalExpected = `# HELP agones_gameservers_total The total of gameservers +# TYPE agones_gameservers_total counter +agones_gameservers_total{fleet_name="test",type="Creating"} 16 +agones_gameservers_total{fleet_name="test",type="Scheduled"} 15 +agones_gameservers_total{fleet_name="test",type="Starting"} 10 +agones_gameservers_total{fleet_name="test",type="Unhealthy"} 1 +agones_gameservers_total{fleet_name="none",type="Creating"} 19 +agones_gameservers_total{fleet_name="none",type="Scheduled"} 18 +agones_gameservers_total{fleet_name="none",type="Starting"} 16 +agones_gameservers_total{fleet_name="none",type="Unhealthy"} 1 +` + +var fleetReplicasCountExpected = `# HELP agones_fleets_replicas_count The number of replicas per fleet +# TYPE agones_fleets_replicas_count gauge +agones_fleets_replicas_count{name="fleet-deleted",type="allocated"} 0 +agones_fleets_replicas_count{name="fleet-deleted",type="desired"} 0 +agones_fleets_replicas_count{name="fleet-deleted",type="ready"} 0 +agones_fleets_replicas_count{name="fleet-deleted",type="total"} 0 +agones_fleets_replicas_count{name="fleet-test",type="allocated"} 2 +agones_fleets_replicas_count{name="fleet-test",type="desired"} 5 +agones_fleets_replicas_count{name="fleet-test",type="ready"} 1 +agones_fleets_replicas_count{name="fleet-test",type="total"} 8 +` + +var fasStateExpected = `# HELP agones_fleet_autoscalers_able_to_scale The fleet autoscaler can access the fleet to scale +# TYPE agones_fleet_autoscalers_able_to_scale gauge +agones_fleet_autoscalers_able_to_scale{fleet_name="first-fleet",name="name-switch"} 0 +agones_fleet_autoscalers_able_to_scale{fleet_name="second-fleet",name="name-switch"} 1 +agones_fleet_autoscalers_able_to_scale{fleet_name="deleted-fleet",name="deleted"} 0 +# HELP agones_fleet_autoscalers_buffer_limits The limits of buffer based fleet autoscalers +# TYPE agones_fleet_autoscalers_buffer_limits gauge +agones_fleet_autoscalers_buffer_limits{fleet_name="first-fleet",name="name-switch",type="max"} 50 +agones_fleet_autoscalers_buffer_limits{fleet_name="first-fleet",name="name-switch",type="min"} 10 +agones_fleet_autoscalers_buffer_limits{fleet_name="second-fleet",name="name-switch",type="max"} 50 +agones_fleet_autoscalers_buffer_limits{fleet_name="second-fleet",name="name-switch",type="min"} 10 +agones_fleet_autoscalers_buffer_limits{fleet_name="deleted-fleet",name="deleted",type="max"} 150 +agones_fleet_autoscalers_buffer_limits{fleet_name="deleted-fleet",name="deleted",type="min"} 15 +# HELP agones_fleet_autoscalers_buffer_size The buffer size of fleet autoscalers +# TYPE agones_fleet_autoscalers_buffer_size gauge +agones_fleet_autoscalers_buffer_size{fleet_name="first-fleet",name="name-switch",type="count"} 10 +agones_fleet_autoscalers_buffer_size{fleet_name="second-fleet",name="name-switch",type="count"} 10 +agones_fleet_autoscalers_buffer_size{fleet_name="deleted-fleet",name="deleted",type="percentage"} 50 +# HELP agones_fleet_autoscalers_current_replicas_count The current replicas count as seen by autoscalers +# TYPE agones_fleet_autoscalers_current_replicas_count gauge +agones_fleet_autoscalers_current_replicas_count{fleet_name="first-fleet",name="name-switch"} 0 +agones_fleet_autoscalers_current_replicas_count{fleet_name="second-fleet",name="name-switch"} 20 +agones_fleet_autoscalers_current_replicas_count{fleet_name="deleted-fleet",name="deleted"} 0 +# HELP agones_fleet_autoscalers_desired_replicas_count The desired replicas count as seen by autoscalers +# TYPE agones_fleet_autoscalers_desired_replicas_count gauge +agones_fleet_autoscalers_desired_replicas_count{fleet_name="first-fleet",name="name-switch"} 0 +agones_fleet_autoscalers_desired_replicas_count{fleet_name="second-fleet",name="name-switch"} 10 +agones_fleet_autoscalers_desired_replicas_count{fleet_name="deleted-fleet",name="deleted"} 0 +# HELP agones_fleet_autoscalers_limited The fleet autoscaler is capped +# TYPE agones_fleet_autoscalers_limited gauge +agones_fleet_autoscalers_limited{fleet_name="first-fleet",name="name-switch"} 0 +agones_fleet_autoscalers_limited{fleet_name="second-fleet",name="name-switch"} 1 +agones_fleet_autoscalers_limited{fleet_name="deleted-fleet",name="deleted"} 0 +` diff --git a/vendor/github.com/google/uuid/CONTRIBUTING.md b/vendor/github.com/google/uuid/CONTRIBUTING.md new file mode 100644 index 0000000000..04fdf09f13 --- /dev/null +++ b/vendor/github.com/google/uuid/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# How to contribute + +We definitely welcome patches and contribution to this project! + +### Legal requirements + +In order to protect both you and ourselves, you will need to sign the +[Contributor License Agreement](https://cla.developers.google.com/clas). + +You may have already signed it for other Google projects. diff --git a/vendor/github.com/google/uuid/CONTRIBUTORS b/vendor/github.com/google/uuid/CONTRIBUTORS new file mode 100644 index 0000000000..b4bb97f6bc --- /dev/null +++ b/vendor/github.com/google/uuid/CONTRIBUTORS @@ -0,0 +1,9 @@ +Paul Borman +bmatsuo +shawnps +theory +jboverfelt +dsymonds +cd1 +wallclockbuilder +dansouza diff --git a/vendor/github.com/google/uuid/LICENSE b/vendor/github.com/google/uuid/LICENSE new file mode 100644 index 0000000000..5dc68268d9 --- /dev/null +++ b/vendor/github.com/google/uuid/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009,2014 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/google/uuid/README.md b/vendor/github.com/google/uuid/README.md new file mode 100644 index 0000000000..9d92c11f16 --- /dev/null +++ b/vendor/github.com/google/uuid/README.md @@ -0,0 +1,19 @@ +# uuid ![build status](https://travis-ci.org/google/uuid.svg?branch=master) +The uuid package generates and inspects UUIDs based on +[RFC 4122](http://tools.ietf.org/html/rfc4122) +and DCE 1.1: Authentication and Security Services. + +This package is based on the github.com/pborman/uuid package (previously named +code.google.com/p/go-uuid). It differs from these earlier packages in that +a UUID is a 16 byte array rather than a byte slice. One loss due to this +change is the ability to represent an invalid UUID (vs a NIL UUID). + +###### Install +`go get github.com/google/uuid` + +###### Documentation +[![GoDoc](https://godoc.org/github.com/google/uuid?status.svg)](http://godoc.org/github.com/google/uuid) + +Full `go doc` style documentation for the package can be viewed online without +installing this package by using the GoDoc site here: +http://godoc.org/github.com/google/uuid diff --git a/vendor/github.com/google/uuid/dce.go b/vendor/github.com/google/uuid/dce.go new file mode 100644 index 0000000000..fa820b9d30 --- /dev/null +++ b/vendor/github.com/google/uuid/dce.go @@ -0,0 +1,80 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "fmt" + "os" +) + +// A Domain represents a Version 2 domain +type Domain byte + +// Domain constants for DCE Security (Version 2) UUIDs. +const ( + Person = Domain(0) + Group = Domain(1) + Org = Domain(2) +) + +// NewDCESecurity returns a DCE Security (Version 2) UUID. +// +// The domain should be one of Person, Group or Org. +// On a POSIX system the id should be the users UID for the Person +// domain and the users GID for the Group. The meaning of id for +// the domain Org or on non-POSIX systems is site defined. +// +// For a given domain/id pair the same token may be returned for up to +// 7 minutes and 10 seconds. +func NewDCESecurity(domain Domain, id uint32) (UUID, error) { + uuid, err := NewUUID() + if err == nil { + uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2 + uuid[9] = byte(domain) + binary.BigEndian.PutUint32(uuid[0:], id) + } + return uuid, err +} + +// NewDCEPerson returns a DCE Security (Version 2) UUID in the person +// domain with the id returned by os.Getuid. +// +// NewDCESecurity(Person, uint32(os.Getuid())) +func NewDCEPerson() (UUID, error) { + return NewDCESecurity(Person, uint32(os.Getuid())) +} + +// NewDCEGroup returns a DCE Security (Version 2) UUID in the group +// domain with the id returned by os.Getgid. +// +// NewDCESecurity(Group, uint32(os.Getgid())) +func NewDCEGroup() (UUID, error) { + return NewDCESecurity(Group, uint32(os.Getgid())) +} + +// Domain returns the domain for a Version 2 UUID. Domains are only defined +// for Version 2 UUIDs. +func (uuid UUID) Domain() Domain { + return Domain(uuid[9]) +} + +// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2 +// UUIDs. +func (uuid UUID) ID() uint32 { + return binary.BigEndian.Uint32(uuid[0:4]) +} + +func (d Domain) String() string { + switch d { + case Person: + return "Person" + case Group: + return "Group" + case Org: + return "Org" + } + return fmt.Sprintf("Domain%d", int(d)) +} diff --git a/vendor/github.com/google/uuid/doc.go b/vendor/github.com/google/uuid/doc.go new file mode 100644 index 0000000000..5b8a4b9af8 --- /dev/null +++ b/vendor/github.com/google/uuid/doc.go @@ -0,0 +1,12 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package uuid generates and inspects UUIDs. +// +// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security +// Services. +// +// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to +// maps or compared directly. +package uuid diff --git a/vendor/github.com/google/uuid/go.mod b/vendor/github.com/google/uuid/go.mod new file mode 100644 index 0000000000..fc84cd79d4 --- /dev/null +++ b/vendor/github.com/google/uuid/go.mod @@ -0,0 +1 @@ +module github.com/google/uuid diff --git a/vendor/github.com/google/uuid/hash.go b/vendor/github.com/google/uuid/hash.go new file mode 100644 index 0000000000..b174616315 --- /dev/null +++ b/vendor/github.com/google/uuid/hash.go @@ -0,0 +1,53 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "crypto/md5" + "crypto/sha1" + "hash" +) + +// Well known namespace IDs and UUIDs +var ( + NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) + Nil UUID // empty UUID, all zeros +) + +// NewHash returns a new UUID derived from the hash of space concatenated with +// data generated by h. The hash should be at least 16 byte in length. The +// first 16 bytes of the hash are used to form the UUID. The version of the +// UUID will be the lower 4 bits of version. NewHash is used to implement +// NewMD5 and NewSHA1. +func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { + h.Reset() + h.Write(space[:]) + h.Write(data) + s := h.Sum(nil) + var uuid UUID + copy(uuid[:], s) + uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) + uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant + return uuid +} + +// NewMD5 returns a new MD5 (Version 3) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(md5.New(), space, data, 3) +func NewMD5(space UUID, data []byte) UUID { + return NewHash(md5.New(), space, data, 3) +} + +// NewSHA1 returns a new SHA1 (Version 5) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(sha1.New(), space, data, 5) +func NewSHA1(space UUID, data []byte) UUID { + return NewHash(sha1.New(), space, data, 5) +} diff --git a/vendor/github.com/google/uuid/json_test.go b/vendor/github.com/google/uuid/json_test.go new file mode 100644 index 0000000000..245f91edfb --- /dev/null +++ b/vendor/github.com/google/uuid/json_test.go @@ -0,0 +1,62 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/json" + "reflect" + "testing" +) + +var testUUID = Must(Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479")) + +func TestJSON(t *testing.T) { + type S struct { + ID1 UUID + ID2 UUID + } + s1 := S{ID1: testUUID} + data, err := json.Marshal(&s1) + if err != nil { + t.Fatal(err) + } + var s2 S + if err := json.Unmarshal(data, &s2); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(&s1, &s2) { + t.Errorf("got %#v, want %#v", s2, s1) + } +} + +func BenchmarkUUID_MarshalJSON(b *testing.B) { + x := &struct { + UUID UUID `json:"uuid"` + }{} + var err error + x.UUID, err = Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479") + if err != nil { + b.Fatal(err) + } + for i := 0; i < b.N; i++ { + js, err := json.Marshal(x) + if err != nil { + b.Fatalf("marshal json: %#v (%v)", js, err) + } + } +} + +func BenchmarkUUID_UnmarshalJSON(b *testing.B) { + js := []byte(`{"uuid":"f47ac10b-58cc-0372-8567-0e02b2c3d479"}`) + var x *struct { + UUID UUID `json:"uuid"` + } + for i := 0; i < b.N; i++ { + err := json.Unmarshal(js, &x) + if err != nil { + b.Fatalf("marshal json: %#v (%v)", js, err) + } + } +} diff --git a/vendor/github.com/google/uuid/marshal.go b/vendor/github.com/google/uuid/marshal.go new file mode 100644 index 0000000000..7f9e0c6c0e --- /dev/null +++ b/vendor/github.com/google/uuid/marshal.go @@ -0,0 +1,37 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "fmt" + +// MarshalText implements encoding.TextMarshaler. +func (uuid UUID) MarshalText() ([]byte, error) { + var js [36]byte + encodeHex(js[:], uuid) + return js[:], nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (uuid *UUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err == nil { + *uuid = id + } + return err +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (uuid UUID) MarshalBinary() ([]byte, error) { + return uuid[:], nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (uuid *UUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(uuid[:], data) + return nil +} diff --git a/vendor/github.com/google/uuid/node.go b/vendor/github.com/google/uuid/node.go new file mode 100644 index 0000000000..3e4e90dc44 --- /dev/null +++ b/vendor/github.com/google/uuid/node.go @@ -0,0 +1,89 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "sync" +) + +var ( + nodeMu sync.Mutex + ifname string // name of interface being used + nodeID [6]byte // hardware for version 1 UUIDs + zeroID [6]byte // nodeID with only 0's +) + +// NodeInterface returns the name of the interface from which the NodeID was +// derived. The interface "user" is returned if the NodeID was set by +// SetNodeID. +func NodeInterface() string { + defer nodeMu.Unlock() + nodeMu.Lock() + return ifname +} + +// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs. +// If name is "" then the first usable interface found will be used or a random +// Node ID will be generated. If a named interface cannot be found then false +// is returned. +// +// SetNodeInterface never fails when name is "". +func SetNodeInterface(name string) bool { + defer nodeMu.Unlock() + nodeMu.Lock() + return setNodeInterface(name) +} + +func setNodeInterface(name string) bool { + iname, addr := getHardwareInterface(name) // null implementation for js + if iname != "" && addr != nil { + ifname = iname + copy(nodeID[:], addr) + return true + } + + // We found no interfaces with a valid hardware address. If name + // does not specify a specific interface generate a random Node ID + // (section 4.1.6) + if name == "" { + randomBits(nodeID[:]) + return true + } + return false +} + +// NodeID returns a slice of a copy of the current Node ID, setting the Node ID +// if not already set. +func NodeID() []byte { + defer nodeMu.Unlock() + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + nid := nodeID + return nid[:] +} + +// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes +// of id are used. If id is less than 6 bytes then false is returned and the +// Node ID is not set. +func SetNodeID(id []byte) bool { + if len(id) < 6 { + return false + } + defer nodeMu.Unlock() + nodeMu.Lock() + copy(nodeID[:], id) + ifname = "user" + return true +} + +// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is +// not valid. The NodeID is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) NodeID() []byte { + var node [6]byte + copy(node[:], uuid[10:]) + return node[:] +} diff --git a/vendor/github.com/google/uuid/node_js.go b/vendor/github.com/google/uuid/node_js.go new file mode 100644 index 0000000000..24b78edc90 --- /dev/null +++ b/vendor/github.com/google/uuid/node_js.go @@ -0,0 +1,12 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build js + +package uuid + +// getHardwareInterface returns nil values for the JS version of the code. +// This remvoves the "net" dependency, because it is not used in the browser. +// Using the "net" library inflates the size of the transpiled JS code by 673k bytes. +func getHardwareInterface(name string) (string, []byte) { return "", nil } diff --git a/vendor/github.com/google/uuid/node_net.go b/vendor/github.com/google/uuid/node_net.go new file mode 100644 index 0000000000..0cbbcddbd6 --- /dev/null +++ b/vendor/github.com/google/uuid/node_net.go @@ -0,0 +1,33 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !js + +package uuid + +import "net" + +var interfaces []net.Interface // cached list of interfaces + +// getHardwareInterface returns the name and hardware address of interface name. +// If name is "" then the name and hardware address of one of the system's +// interfaces is returned. If no interfaces are found (name does not exist or +// there are no interfaces) then "", nil is returned. +// +// Only addresses of at least 6 bytes are returned. +func getHardwareInterface(name string) (string, []byte) { + if interfaces == nil { + var err error + interfaces, err = net.Interfaces() + if err != nil { + return "", nil + } + } + for _, ifs := range interfaces { + if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) { + return ifs.Name, ifs.HardwareAddr + } + } + return "", nil +} diff --git a/vendor/github.com/google/uuid/seq_test.go b/vendor/github.com/google/uuid/seq_test.go new file mode 100644 index 0000000000..4f6c549125 --- /dev/null +++ b/vendor/github.com/google/uuid/seq_test.go @@ -0,0 +1,66 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "flag" + "runtime" + "testing" + "time" +) + +// This test is only run when --regressions is passed on the go test line. +var regressions = flag.Bool("regressions", false, "run uuid regression tests") + +// TestClockSeqRace tests for a particular race condition of returning two +// identical Version1 UUIDs. The duration of 1 minute was chosen as the race +// condition, before being fixed, nearly always occurred in under 30 seconds. +func TestClockSeqRace(t *testing.T) { + if !*regressions { + t.Skip("skipping regression tests") + } + duration := time.Minute + + done := make(chan struct{}) + defer close(done) + + ch := make(chan UUID, 10000) + ncpu := runtime.NumCPU() + switch ncpu { + case 0, 1: + // We can't run the test effectively. + t.Skip("skipping race test, only one CPU detected") + return + default: + runtime.GOMAXPROCS(ncpu) + } + for i := 0; i < ncpu; i++ { + go func() { + for { + select { + case <-done: + return + case ch <- Must(NewUUID()): + } + } + }() + } + + uuids := make(map[string]bool) + cnt := 0 + start := time.Now() + for u := range ch { + s := u.String() + if uuids[s] { + t.Errorf("duplicate uuid after %d in %v: %s", cnt, time.Since(start), s) + return + } + uuids[s] = true + if time.Since(start) > duration { + return + } + cnt++ + } +} diff --git a/vendor/github.com/google/uuid/sql.go b/vendor/github.com/google/uuid/sql.go new file mode 100644 index 0000000000..f326b54db3 --- /dev/null +++ b/vendor/github.com/google/uuid/sql.go @@ -0,0 +1,59 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements sql.Scanner so UUIDs can be read from databases transparently +// Currently, database types that map to string and []byte are supported. Please +// consult database-specific driver documentation for matching types. +func (uuid *UUID) Scan(src interface{}) error { + switch src := src.(type) { + case nil: + return nil + + case string: + // if an empty UUID comes from a table, we return a null UUID + if src == "" { + return nil + } + + // see Parse for required string format + u, err := Parse(src) + if err != nil { + return fmt.Errorf("Scan: %v", err) + } + + *uuid = u + + case []byte: + // if an empty UUID comes from a table, we return a null UUID + if len(src) == 0 { + return nil + } + + // assumes a simple slice of bytes if 16 bytes + // otherwise attempts to parse + if len(src) != 16 { + return uuid.Scan(string(src)) + } + copy((*uuid)[:], src) + + default: + return fmt.Errorf("Scan: unable to scan type %T into UUID", src) + } + + return nil +} + +// Value implements sql.Valuer so that UUIDs can be written to databases +// transparently. Currently, UUIDs map to strings. Please consult +// database-specific driver documentation for matching types. +func (uuid UUID) Value() (driver.Value, error) { + return uuid.String(), nil +} diff --git a/vendor/github.com/google/uuid/sql_test.go b/vendor/github.com/google/uuid/sql_test.go new file mode 100644 index 0000000000..1803dfd879 --- /dev/null +++ b/vendor/github.com/google/uuid/sql_test.go @@ -0,0 +1,113 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "strings" + "testing" +) + +func TestScan(t *testing.T) { + stringTest := "f47ac10b-58cc-0372-8567-0e02b2c3d479" + badTypeTest := 6 + invalidTest := "f47ac10b-58cc-0372-8567-0e02b2c3d4" + + byteTest := make([]byte, 16) + byteTestUUID := Must(Parse(stringTest)) + copy(byteTest, byteTestUUID[:]) + + // sunny day tests + + var uuid UUID + err := (&uuid).Scan(stringTest) + if err != nil { + t.Fatal(err) + } + + err = (&uuid).Scan([]byte(stringTest)) + if err != nil { + t.Fatal(err) + } + + err = (&uuid).Scan(byteTest) + if err != nil { + t.Fatal(err) + } + + // bad type tests + + err = (&uuid).Scan(badTypeTest) + if err == nil { + t.Error("int correctly parsed and shouldn't have") + } + if !strings.Contains(err.Error(), "unable to scan type") { + t.Error("attempting to parse an int returned an incorrect error message") + } + + // invalid/incomplete uuids + + err = (&uuid).Scan(invalidTest) + if err == nil { + t.Error("invalid uuid was parsed without error") + } + if !strings.Contains(err.Error(), "invalid UUID") { + t.Error("attempting to parse an invalid UUID returned an incorrect error message") + } + + err = (&uuid).Scan(byteTest[:len(byteTest)-2]) + if err == nil { + t.Error("invalid byte uuid was parsed without error") + } + if !strings.Contains(err.Error(), "invalid UUID") { + t.Error("attempting to parse an invalid byte UUID returned an incorrect error message") + } + + // empty tests + + uuid = UUID{} + var emptySlice []byte + err = (&uuid).Scan(emptySlice) + if err != nil { + t.Fatal(err) + } + + for _, v := range uuid { + if v != 0 { + t.Error("UUID was not nil after scanning empty byte slice") + } + } + + uuid = UUID{} + var emptyString string + err = (&uuid).Scan(emptyString) + if err != nil { + t.Fatal(err) + } + for _, v := range uuid { + if v != 0 { + t.Error("UUID was not nil after scanning empty byte slice") + } + } + + uuid = UUID{} + err = (&uuid).Scan(nil) + if err != nil { + t.Fatal(err) + } + for _, v := range uuid { + if v != 0 { + t.Error("UUID was not nil after scanning nil") + } + } +} + +func TestValue(t *testing.T) { + stringTest := "f47ac10b-58cc-0372-8567-0e02b2c3d479" + uuid := Must(Parse(stringTest)) + val, _ := uuid.Value() + if val != stringTest { + t.Error("Value() did not return expected string") + } +} diff --git a/vendor/github.com/google/uuid/time.go b/vendor/github.com/google/uuid/time.go new file mode 100644 index 0000000000..e6ef06cdc8 --- /dev/null +++ b/vendor/github.com/google/uuid/time.go @@ -0,0 +1,123 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "sync" + "time" +) + +// A Time represents a time as the number of 100's of nanoseconds since 15 Oct +// 1582. +type Time int64 + +const ( + lillian = 2299160 // Julian day of 15 Oct 1582 + unix = 2440587 // Julian day of 1 Jan 1970 + epoch = unix - lillian // Days between epochs + g1582 = epoch * 86400 // seconds between epochs + g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs +) + +var ( + timeMu sync.Mutex + lasttime uint64 // last time we returned + clockSeq uint16 // clock sequence for this run + + timeNow = time.Now // for testing +) + +// UnixTime converts t the number of seconds and nanoseconds using the Unix +// epoch of 1 Jan 1970. +func (t Time) UnixTime() (sec, nsec int64) { + sec = int64(t - g1582ns100) + nsec = (sec % 10000000) * 100 + sec /= 10000000 + return sec, nsec +} + +// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and +// clock sequence as well as adjusting the clock sequence as needed. An error +// is returned if the current time cannot be determined. +func GetTime() (Time, uint16, error) { + defer timeMu.Unlock() + timeMu.Lock() + return getTime() +} + +func getTime() (Time, uint16, error) { + t := timeNow() + + // If we don't have a clock sequence already, set one. + if clockSeq == 0 { + setClockSequence(-1) + } + now := uint64(t.UnixNano()/100) + g1582ns100 + + // If time has gone backwards with this clock sequence then we + // increment the clock sequence + if now <= lasttime { + clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000 + } + lasttime = now + return Time(now), clockSeq, nil +} + +// ClockSequence returns the current clock sequence, generating one if not +// already set. The clock sequence is only used for Version 1 UUIDs. +// +// The uuid package does not use global static storage for the clock sequence or +// the last time a UUID was generated. Unless SetClockSequence is used, a new +// random clock sequence is generated the first time a clock sequence is +// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) +func ClockSequence() int { + defer timeMu.Unlock() + timeMu.Lock() + return clockSequence() +} + +func clockSequence() int { + if clockSeq == 0 { + setClockSequence(-1) + } + return int(clockSeq & 0x3fff) +} + +// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to +// -1 causes a new sequence to be generated. +func SetClockSequence(seq int) { + defer timeMu.Unlock() + timeMu.Lock() + setClockSequence(seq) +} + +func setClockSequence(seq int) { + if seq == -1 { + var b [2]byte + randomBits(b[:]) // clock sequence + seq = int(b[0])<<8 | int(b[1]) + } + oldSeq := clockSeq + clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant + if oldSeq != clockSeq { + lasttime = 0 + } +} + +// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in +// uuid. The time is only defined for version 1 and 2 UUIDs. +func (uuid UUID) Time() Time { + time := int64(binary.BigEndian.Uint32(uuid[0:4])) + time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32 + time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48 + return Time(time) +} + +// ClockSequence returns the clock sequence encoded in uuid. +// The clock sequence is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) ClockSequence() int { + return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff +} diff --git a/vendor/github.com/google/uuid/util.go b/vendor/github.com/google/uuid/util.go new file mode 100644 index 0000000000..5ea6c73780 --- /dev/null +++ b/vendor/github.com/google/uuid/util.go @@ -0,0 +1,43 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "io" +) + +// randomBits completely fills slice b with random data. +func randomBits(b []byte) { + if _, err := io.ReadFull(rander, b); err != nil { + panic(err.Error()) // rand should never fail + } +} + +// xvalues returns the value of a byte as a hexadecimal digit or 255. +var xvalues = [256]byte{ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +} + +// xtob converts hex characters x1 and x2 into a byte. +func xtob(x1, x2 byte) (byte, bool) { + b1 := xvalues[x1] + b2 := xvalues[x2] + return (b1 << 4) | b2, b1 != 255 && b2 != 255 +} diff --git a/vendor/github.com/google/uuid/uuid.go b/vendor/github.com/google/uuid/uuid.go new file mode 100644 index 0000000000..524404cc52 --- /dev/null +++ b/vendor/github.com/google/uuid/uuid.go @@ -0,0 +1,245 @@ +// Copyright 2018 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "strings" +) + +// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC +// 4122. +type UUID [16]byte + +// A Version represents a UUID's version. +type Version byte + +// A Variant represents a UUID's variant. +type Variant byte + +// Constants returned by Variant. +const ( + Invalid = Variant(iota) // Invalid UUID + RFC4122 // The variant specified in RFC4122 + Reserved // Reserved, NCS backward compatibility. + Microsoft // Reserved, Microsoft Corporation backward compatibility. + Future // Reserved for future definition. +) + +var rander = rand.Reader // random function + +// Parse decodes s into a UUID or returns an error. Both the standard UUID +// forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the +// Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex +// encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. +func Parse(s string) (UUID, error) { + var uuid UUID + switch len(s) { + // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36: + + // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: + if strings.ToLower(s[:9]) != "urn:uuid:" { + return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) + } + s = s[9:] + + // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + case 36 + 2: + s = s[1:] + + // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + case 32: + var ok bool + for i := range uuid { + uuid[i], ok = xtob(s[i*2], s[i*2+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, fmt.Errorf("invalid UUID length: %d", len(s)) + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34} { + v, ok := xtob(s[x], s[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// ParseBytes is like Parse, except it parses a byte slice instead of a string. +func ParseBytes(b []byte) (UUID, error) { + var uuid UUID + switch len(b) { + case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) { + return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) + } + b = b[9:] + case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + b = b[1:] + case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + var ok bool + for i := 0; i < 32; i += 2 { + uuid[i/2], ok = xtob(b[i], b[i+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, fmt.Errorf("invalid UUID length: %d", len(b)) + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34} { + v, ok := xtob(b[x], b[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// MustParse is like Parse but panics if the string cannot be parsed. +// It simplifies safe initialization of global variables holding compiled UUIDs. +func MustParse(s string) UUID { + uuid, err := Parse(s) + if err != nil { + panic(`uuid: Parse(` + s + `): ` + err.Error()) + } + return uuid +} + +// FromBytes creates a new UUID from a byte slice. Returns an error if the slice +// does not have a length of 16. The bytes are copied from the slice. +func FromBytes(b []byte) (uuid UUID, err error) { + err = uuid.UnmarshalBinary(b) + return uuid, err +} + +// Must returns uuid if err is nil and panics otherwise. +func Must(uuid UUID, err error) UUID { + if err != nil { + panic(err) + } + return uuid +} + +// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// , or "" if uuid is invalid. +func (uuid UUID) String() string { + var buf [36]byte + encodeHex(buf[:], uuid) + return string(buf[:]) +} + +// URN returns the RFC 2141 URN form of uuid, +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid. +func (uuid UUID) URN() string { + var buf [36 + 9]byte + copy(buf[:], "urn:uuid:") + encodeHex(buf[9:], uuid) + return string(buf[:]) +} + +func encodeHex(dst []byte, uuid UUID) { + hex.Encode(dst, uuid[:4]) + dst[8] = '-' + hex.Encode(dst[9:13], uuid[4:6]) + dst[13] = '-' + hex.Encode(dst[14:18], uuid[6:8]) + dst[18] = '-' + hex.Encode(dst[19:23], uuid[8:10]) + dst[23] = '-' + hex.Encode(dst[24:], uuid[10:]) +} + +// Variant returns the variant encoded in uuid. +func (uuid UUID) Variant() Variant { + switch { + case (uuid[8] & 0xc0) == 0x80: + return RFC4122 + case (uuid[8] & 0xe0) == 0xc0: + return Microsoft + case (uuid[8] & 0xe0) == 0xe0: + return Future + default: + return Reserved + } +} + +// Version returns the version of uuid. +func (uuid UUID) Version() Version { + return Version(uuid[6] >> 4) +} + +func (v Version) String() string { + if v > 15 { + return fmt.Sprintf("BAD_VERSION_%d", v) + } + return fmt.Sprintf("VERSION_%d", v) +} + +func (v Variant) String() string { + switch v { + case RFC4122: + return "RFC4122" + case Reserved: + return "Reserved" + case Microsoft: + return "Microsoft" + case Future: + return "Future" + case Invalid: + return "Invalid" + } + return fmt.Sprintf("BadVariant%d", int(v)) +} + +// SetRand sets the random number generator to r, which implements io.Reader. +// If r.Read returns an error when the package requests random data then +// a panic will be issued. +// +// Calling SetRand with nil sets the random number generator to the default +// generator. +func SetRand(r io.Reader) { + if r == nil { + rander = rand.Reader + return + } + rander = r +} diff --git a/vendor/github.com/google/uuid/uuid_test.go b/vendor/github.com/google/uuid/uuid_test.go new file mode 100644 index 0000000000..e7876f151a --- /dev/null +++ b/vendor/github.com/google/uuid/uuid_test.go @@ -0,0 +1,559 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "fmt" + "os" + "runtime" + "strings" + "testing" + "time" + "unsafe" +) + +type test struct { + in string + version Version + variant Variant + isuuid bool +} + +var tests = []test{ + {"f47ac10b-58cc-0372-8567-0e02b2c3d479", 0, RFC4122, true}, + {"f47ac10b-58cc-1372-8567-0e02b2c3d479", 1, RFC4122, true}, + {"f47ac10b-58cc-2372-8567-0e02b2c3d479", 2, RFC4122, true}, + {"f47ac10b-58cc-3372-8567-0e02b2c3d479", 3, RFC4122, true}, + {"f47ac10b-58cc-4372-8567-0e02b2c3d479", 4, RFC4122, true}, + {"f47ac10b-58cc-5372-8567-0e02b2c3d479", 5, RFC4122, true}, + {"f47ac10b-58cc-6372-8567-0e02b2c3d479", 6, RFC4122, true}, + {"f47ac10b-58cc-7372-8567-0e02b2c3d479", 7, RFC4122, true}, + {"f47ac10b-58cc-8372-8567-0e02b2c3d479", 8, RFC4122, true}, + {"f47ac10b-58cc-9372-8567-0e02b2c3d479", 9, RFC4122, true}, + {"f47ac10b-58cc-a372-8567-0e02b2c3d479", 10, RFC4122, true}, + {"f47ac10b-58cc-b372-8567-0e02b2c3d479", 11, RFC4122, true}, + {"f47ac10b-58cc-c372-8567-0e02b2c3d479", 12, RFC4122, true}, + {"f47ac10b-58cc-d372-8567-0e02b2c3d479", 13, RFC4122, true}, + {"f47ac10b-58cc-e372-8567-0e02b2c3d479", 14, RFC4122, true}, + {"f47ac10b-58cc-f372-8567-0e02b2c3d479", 15, RFC4122, true}, + + {"urn:uuid:f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true}, + {"URN:UUID:f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-1567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-2567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-3567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-4567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-5567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-6567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-7567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-8567-0e02b2c3d479", 4, RFC4122, true}, + {"f47ac10b-58cc-4372-9567-0e02b2c3d479", 4, RFC4122, true}, + {"f47ac10b-58cc-4372-a567-0e02b2c3d479", 4, RFC4122, true}, + {"f47ac10b-58cc-4372-b567-0e02b2c3d479", 4, RFC4122, true}, + {"f47ac10b-58cc-4372-c567-0e02b2c3d479", 4, Microsoft, true}, + {"f47ac10b-58cc-4372-d567-0e02b2c3d479", 4, Microsoft, true}, + {"f47ac10b-58cc-4372-e567-0e02b2c3d479", 4, Future, true}, + {"f47ac10b-58cc-4372-f567-0e02b2c3d479", 4, Future, true}, + + + {"f47ac10b158cc-5372-a567-0e02b2c3d479", 0, Invalid, false}, + {"f47ac10b-58cc25372-a567-0e02b2c3d479", 0, Invalid, false}, + {"f47ac10b-58cc-53723a567-0e02b2c3d479", 0, Invalid, false}, + {"f47ac10b-58cc-5372-a56740e02b2c3d479", 0, Invalid, false}, + {"f47ac10b-58cc-5372-a567-0e02-2c3d479", 0, Invalid, false}, + {"g47ac10b-58cc-4372-a567-0e02b2c3d479", 0, Invalid, false}, + + + {"{f47ac10b-58cc-0372-8567-0e02b2c3d479}", 0, RFC4122, true}, + {"{f47ac10b-58cc-0372-8567-0e02b2c3d479", 0, Invalid, false}, + {"f47ac10b-58cc-0372-8567-0e02b2c3d479}", 0, Invalid, false}, + + {"f47ac10b58cc037285670e02b2c3d479", 0, RFC4122, true}, + {"f47ac10b58cc037285670e02b2c3d4790", 0, Invalid, false}, + {"f47ac10b58cc037285670e02b2c3d47", 0, Invalid, false}, +} + +var constants = []struct { + c interface{} + name string +}{ + {Person, "Person"}, + {Group, "Group"}, + {Org, "Org"}, + {Invalid, "Invalid"}, + {RFC4122, "RFC4122"}, + {Reserved, "Reserved"}, + {Microsoft, "Microsoft"}, + {Future, "Future"}, + {Domain(17), "Domain17"}, + {Variant(42), "BadVariant42"}, +} + +func testTest(t *testing.T, in string, tt test) { + uuid, err := Parse(in) + if ok := (err == nil); ok != tt.isuuid { + t.Errorf("Parse(%s) got %v expected %v\b", in, ok, tt.isuuid) + } + if err != nil { + return + } + + if v := uuid.Variant(); v != tt.variant { + t.Errorf("Variant(%s) got %d expected %d\b", in, v, tt.variant) + } + if v := uuid.Version(); v != tt.version { + t.Errorf("Version(%s) got %d expected %d\b", in, v, tt.version) + } +} + +func testBytes(t *testing.T, in []byte, tt test) { + uuid, err := ParseBytes(in) + if ok := (err == nil); ok != tt.isuuid { + t.Errorf("ParseBytes(%s) got %v expected %v\b", in, ok, tt.isuuid) + } + if err != nil { + return + } + suuid, _ := Parse(string(in)) + if uuid != suuid { + t.Errorf("ParseBytes(%s) got %v expected %v\b", in, uuid, suuid) + } +} + +func TestUUID(t *testing.T) { + for _, tt := range tests { + testTest(t, tt.in, tt) + testTest(t, strings.ToUpper(tt.in), tt) + testBytes(t, []byte(tt.in), tt) + } +} + +func TestFromBytes(t *testing.T) { + b := []byte{ + 0x7d, 0x44, 0x48, 0x40, + 0x9d, 0xc0, + 0x11, 0xd1, + 0xb2, 0x45, + 0x5f, 0xfd, 0xce, 0x74, 0xfa, 0xd2, + } + uuid, err := FromBytes(b) + if err != nil { + t.Fatalf("%s", err) + } + for i := 0; i < len(uuid); i++ { + if b[i] != uuid[i] { + t.Fatalf("FromBytes() got %v expected %v\b", uuid[:], b) + } + } +} + +func TestConstants(t *testing.T) { + for x, tt := range constants { + v, ok := tt.c.(fmt.Stringer) + if !ok { + t.Errorf("%x: %v: not a stringer", x, v) + } else if s := v.String(); s != tt.name { + v, _ := tt.c.(int) + t.Errorf("%x: Constant %T:%d gives %q, expected %q", x, tt.c, v, s, tt.name) + } + } +} + +func TestRandomUUID(t *testing.T) { + m := make(map[string]bool) + for x := 1; x < 32; x++ { + uuid := New() + s := uuid.String() + if m[s] { + t.Errorf("NewRandom returned duplicated UUID %s", s) + } + m[s] = true + if v := uuid.Version(); v != 4 { + t.Errorf("Random UUID of version %s", v) + } + if uuid.Variant() != RFC4122 { + t.Errorf("Random UUID is variant %d", uuid.Variant()) + } + } +} + +func TestNew(t *testing.T) { + m := make(map[UUID]bool) + for x := 1; x < 32; x++ { + s := New() + if m[s] { + t.Errorf("New returned duplicated UUID %s", s) + } + m[s] = true + uuid, err := Parse(s.String()) + if err != nil { + t.Errorf("New.String() returned %q which does not decode", s) + continue + } + if v := uuid.Version(); v != 4 { + t.Errorf("Random UUID of version %s", v) + } + if uuid.Variant() != RFC4122 { + t.Errorf("Random UUID is variant %d", uuid.Variant()) + } + } +} + +func TestClockSeq(t *testing.T) { + // Fake time.Now for this test to return a monotonically advancing time; restore it at end. + defer func(orig func() time.Time) { timeNow = orig }(timeNow) + monTime := time.Now() + timeNow = func() time.Time { + monTime = monTime.Add(1 * time.Second) + return monTime + } + + SetClockSequence(-1) + uuid1, err := NewUUID() + if err != nil { + t.Fatalf("could not create UUID: %v", err) + } + uuid2, err := NewUUID() + if err != nil { + t.Fatalf("could not create UUID: %v", err) + } + + if s1, s2 := uuid1.ClockSequence(), uuid2.ClockSequence(); s1 != s2 { + t.Errorf("clock sequence %d != %d", s1, s2) + } + + SetClockSequence(-1) + uuid2, err = NewUUID() + if err != nil { + t.Fatalf("could not create UUID: %v", err) + } + + // Just on the very off chance we generated the same sequence + // two times we try again. + if uuid1.ClockSequence() == uuid2.ClockSequence() { + SetClockSequence(-1) + uuid2, err = NewUUID() + if err != nil { + t.Fatalf("could not create UUID: %v", err) + } + } + if s1, s2 := uuid1.ClockSequence(), uuid2.ClockSequence(); s1 == s2 { + t.Errorf("Duplicate clock sequence %d", s1) + } + + SetClockSequence(0x1234) + uuid1, err = NewUUID() + if err != nil { + t.Fatalf("could not create UUID: %v", err) + } + if seq := uuid1.ClockSequence(); seq != 0x1234 { + t.Errorf("%s: expected seq 0x1234 got 0x%04x", uuid1, seq) + } +} + +func TestCoding(t *testing.T) { + text := "7d444840-9dc0-11d1-b245-5ffdce74fad2" + urn := "urn:uuid:7d444840-9dc0-11d1-b245-5ffdce74fad2" + data := UUID{ + 0x7d, 0x44, 0x48, 0x40, + 0x9d, 0xc0, + 0x11, 0xd1, + 0xb2, 0x45, + 0x5f, 0xfd, 0xce, 0x74, 0xfa, 0xd2, + } + if v := data.String(); v != text { + t.Errorf("%x: encoded to %s, expected %s", data, v, text) + } + if v := data.URN(); v != urn { + t.Errorf("%x: urn is %s, expected %s", data, v, urn) + } + + uuid, err := Parse(text) + if err != nil { + t.Errorf("Parse returned unexpected error %v", err) + } + if data != uuid { + t.Errorf("%s: decoded to %s, expected %s", text, uuid, data) + } +} + +func TestVersion1(t *testing.T) { + uuid1, err := NewUUID() + if err != nil { + t.Fatalf("could not create UUID: %v", err) + } + uuid2, err := NewUUID() + if err != nil { + t.Fatalf("could not create UUID: %v", err) + } + + if uuid1 == uuid2 { + t.Errorf("%s:duplicate uuid", uuid1) + } + if v := uuid1.Version(); v != 1 { + t.Errorf("%s: version %s expected 1", uuid1, v) + } + if v := uuid2.Version(); v != 1 { + t.Errorf("%s: version %s expected 1", uuid2, v) + } + n1 := uuid1.NodeID() + n2 := uuid2.NodeID() + if !bytes.Equal(n1, n2) { + t.Errorf("Different nodes %x != %x", n1, n2) + } + t1 := uuid1.Time() + t2 := uuid2.Time() + q1 := uuid1.ClockSequence() + q2 := uuid2.ClockSequence() + + switch { + case t1 == t2 && q1 == q2: + t.Error("time stopped") + case t1 > t2 && q1 == q2: + t.Error("time reversed") + case t1 < t2 && q1 != q2: + t.Error("clock sequence changed unexpectedly") + } +} + +func TestNode(t *testing.T) { + // This test is mostly to make sure we don't leave nodeMu locked. + ifname = "" + if ni := NodeInterface(); ni != "" { + t.Errorf("NodeInterface got %q, want %q", ni, "") + } + if SetNodeInterface("xyzzy") { + t.Error("SetNodeInterface succeeded on a bad interface name") + } + if !SetNodeInterface("") { + t.Error("SetNodeInterface failed") + } + if runtime.GOARCH != "js" { + if ni := NodeInterface(); ni == "" { + t.Error("NodeInterface returned an empty string") + } + } + + ni := NodeID() + if len(ni) != 6 { + t.Errorf("ni got %d bytes, want 6", len(ni)) + } + hasData := false + for _, b := range ni { + if b != 0 { + hasData = true + } + } + if !hasData { + t.Error("nodeid is all zeros") + } + + id := []byte{1, 2, 3, 4, 5, 6, 7, 8} + SetNodeID(id) + ni = NodeID() + if !bytes.Equal(ni, id[:6]) { + t.Errorf("got nodeid %v, want %v", ni, id[:6]) + } + + if ni := NodeInterface(); ni != "user" { + t.Errorf("got interface %q, want %q", ni, "user") + } +} + +func TestNodeAndTime(t *testing.T) { + // Time is February 5, 1998 12:30:23.136364800 AM GMT + + uuid, err := Parse("7d444840-9dc0-11d1-b245-5ffdce74fad2") + if err != nil { + t.Fatalf("Parser returned unexpected error %v", err) + } + node := []byte{0x5f, 0xfd, 0xce, 0x74, 0xfa, 0xd2} + + ts := uuid.Time() + c := time.Unix(ts.UnixTime()) + want := time.Date(1998, 2, 5, 0, 30, 23, 136364800, time.UTC) + if !c.Equal(want) { + t.Errorf("Got time %v, want %v", c, want) + } + if !bytes.Equal(node, uuid.NodeID()) { + t.Errorf("Expected node %v got %v", node, uuid.NodeID()) + } +} + +func TestMD5(t *testing.T) { + uuid := NewMD5(NameSpaceDNS, []byte("python.org")).String() + want := "6fa459ea-ee8a-3ca4-894e-db77e160355e" + if uuid != want { + t.Errorf("MD5: got %q expected %q", uuid, want) + } +} + +func TestSHA1(t *testing.T) { + uuid := NewSHA1(NameSpaceDNS, []byte("python.org")).String() + want := "886313e1-3b8a-5372-9b90-0c9aee199e5d" + if uuid != want { + t.Errorf("SHA1: got %q expected %q", uuid, want) + } +} + +func TestNodeID(t *testing.T) { + nid := []byte{1, 2, 3, 4, 5, 6} + SetNodeInterface("") + s := NodeInterface() + if runtime.GOARCH != "js" { + if s == "" || s == "user" { + t.Errorf("NodeInterface %q after SetInterface", s) + } + } + node1 := NodeID() + if node1 == nil { + t.Error("NodeID nil after SetNodeInterface", s) + } + SetNodeID(nid) + s = NodeInterface() + if s != "user" { + t.Errorf("Expected NodeInterface %q got %q", "user", s) + } + node2 := NodeID() + if node2 == nil { + t.Error("NodeID nil after SetNodeID", s) + } + if bytes.Equal(node1, node2) { + t.Error("NodeID not changed after SetNodeID", s) + } else if !bytes.Equal(nid, node2) { + t.Errorf("NodeID is %x, expected %x", node2, nid) + } +} + +func testDCE(t *testing.T, name string, uuid UUID, err error, domain Domain, id uint32) { + if err != nil { + t.Errorf("%s failed: %v", name, err) + return + } + if v := uuid.Version(); v != 2 { + t.Errorf("%s: %s: expected version 2, got %s", name, uuid, v) + return + } + if v := uuid.Domain(); v != domain { + t.Errorf("%s: %s: expected domain %d, got %d", name, uuid, domain, v) + } + if v := uuid.ID(); v != id { + t.Errorf("%s: %s: expected id %d, got %d", name, uuid, id, v) + } +} + +func TestDCE(t *testing.T) { + uuid, err := NewDCESecurity(42, 12345678) + testDCE(t, "NewDCESecurity", uuid, err, 42, 12345678) + uuid, err = NewDCEPerson() + testDCE(t, "NewDCEPerson", uuid, err, Person, uint32(os.Getuid())) + uuid, err = NewDCEGroup() + testDCE(t, "NewDCEGroup", uuid, err, Group, uint32(os.Getgid())) +} + +type badRand struct{} + +func (r badRand) Read(buf []byte) (int, error) { + for i := range buf { + buf[i] = byte(i) + } + return len(buf), nil +} + +func TestBadRand(t *testing.T) { + SetRand(badRand{}) + uuid1 := New() + uuid2 := New() + if uuid1 != uuid2 { + t.Errorf("expected duplicates, got %q and %q", uuid1, uuid2) + } + SetRand(nil) + uuid1 = New() + uuid2 = New() + if uuid1 == uuid2 { + t.Errorf("unexpected duplicates, got %q", uuid1) + } +} + +var asString = "f47ac10b-58cc-0372-8567-0e02b2c3d479" +var asBytes = []byte(asString) + +func BenchmarkParse(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := Parse(asString) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkParseBytes(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := ParseBytes(asBytes) + if err != nil { + b.Fatal(err) + } + } +} + +// parseBytesUnsafe is to benchmark using unsafe. +func parseBytesUnsafe(b []byte) (UUID, error) { + return Parse(*(*string)(unsafe.Pointer(&b))) +} + +func BenchmarkParseBytesUnsafe(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := parseBytesUnsafe(asBytes) + if err != nil { + b.Fatal(err) + } + } +} + +// parseBytesCopy is to benchmark not using unsafe. +func parseBytesCopy(b []byte) (UUID, error) { + return Parse(string(b)) +} + +func BenchmarkParseBytesCopy(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := parseBytesCopy(asBytes) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkNew(b *testing.B) { + for i := 0; i < b.N; i++ { + New() + } +} + +func BenchmarkUUID_String(b *testing.B) { + uuid, err := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479") + if err != nil { + b.Fatal(err) + } + for i := 0; i < b.N; i++ { + if uuid.String() == "" { + b.Fatal("invalid uuid") + } + } +} + +func BenchmarkUUID_URN(b *testing.B) { + uuid, err := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479") + if err != nil { + b.Fatal(err) + } + for i := 0; i < b.N; i++ { + if uuid.URN() == "" { + b.Fatal("invalid uuid") + } + } +} diff --git a/vendor/github.com/google/uuid/version1.go b/vendor/github.com/google/uuid/version1.go new file mode 100644 index 0000000000..199a1ac654 --- /dev/null +++ b/vendor/github.com/google/uuid/version1.go @@ -0,0 +1,44 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" +) + +// NewUUID returns a Version 1 UUID based on the current NodeID and clock +// sequence, and the current time. If the NodeID has not been set by SetNodeID +// or SetNodeInterface then it will be set automatically. If the NodeID cannot +// be set NewUUID returns nil. If clock sequence has not been set by +// SetClockSequence then it will be set automatically. If GetTime fails to +// return the current NewUUID returns nil and an error. +// +// In most cases, New should be used. +func NewUUID() (UUID, error) { + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + nodeMu.Unlock() + + var uuid UUID + now, seq, err := GetTime() + if err != nil { + return uuid, err + } + + timeLow := uint32(now & 0xffffffff) + timeMid := uint16((now >> 32) & 0xffff) + timeHi := uint16((now >> 48) & 0x0fff) + timeHi |= 0x1000 // Version 1 + + binary.BigEndian.PutUint32(uuid[0:], timeLow) + binary.BigEndian.PutUint16(uuid[4:], timeMid) + binary.BigEndian.PutUint16(uuid[6:], timeHi) + binary.BigEndian.PutUint16(uuid[8:], seq) + copy(uuid[10:], nodeID[:]) + + return uuid, nil +} diff --git a/vendor/github.com/google/uuid/version4.go b/vendor/github.com/google/uuid/version4.go new file mode 100644 index 0000000000..84af91c9f5 --- /dev/null +++ b/vendor/github.com/google/uuid/version4.go @@ -0,0 +1,38 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "io" + +// New creates a new random UUID or panics. New is equivalent to +// the expression +// +// uuid.Must(uuid.NewRandom()) +func New() UUID { + return Must(NewRandom()) +} + +// NewRandom returns a Random (Version 4) UUID. +// +// The strength of the UUIDs is based on the strength of the crypto/rand +// package. +// +// A note about uniqueness derived from the UUID Wikipedia entry: +// +// Randomly generated UUIDs have 122 random bits. One's annual risk of being +// hit by a meteorite is estimated to be one chance in 17 billion, that +// means the probability is about 0.00000000006 (6 × 10−11), +// equivalent to the odds of creating a few tens of trillions of UUIDs in a +// year and having one duplicate. +func NewRandom() (UUID, error) { + var uuid UUID + _, err := io.ReadFull(rander, uuid[:]) + if err != nil { + return Nil, err + } + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} diff --git a/vendor/github.com/pborman/uuid/CONTRIBUTING.md b/vendor/github.com/pborman/uuid/CONTRIBUTING.md new file mode 100644 index 0000000000..04fdf09f13 --- /dev/null +++ b/vendor/github.com/pborman/uuid/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# How to contribute + +We definitely welcome patches and contribution to this project! + +### Legal requirements + +In order to protect both you and ourselves, you will need to sign the +[Contributor License Agreement](https://cla.developers.google.com/clas). + +You may have already signed it for other Google projects. diff --git a/vendor/github.com/pborman/uuid/CONTRIBUTORS b/vendor/github.com/pborman/uuid/CONTRIBUTORS new file mode 100644 index 0000000000..b382a04eda --- /dev/null +++ b/vendor/github.com/pborman/uuid/CONTRIBUTORS @@ -0,0 +1 @@ +Paul Borman diff --git a/vendor/github.com/pborman/uuid/LICENSE b/vendor/github.com/pborman/uuid/LICENSE new file mode 100644 index 0000000000..5dc68268d9 --- /dev/null +++ b/vendor/github.com/pborman/uuid/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009,2014 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pborman/uuid/README.md b/vendor/github.com/pborman/uuid/README.md new file mode 100644 index 0000000000..810ad40dc9 --- /dev/null +++ b/vendor/github.com/pborman/uuid/README.md @@ -0,0 +1,15 @@ +This project was automatically exported from code.google.com/p/go-uuid + +# uuid ![build status](https://travis-ci.org/pborman/uuid.svg?branch=master) +The uuid package generates and inspects UUIDs based on [RFC 4122](http://tools.ietf.org/html/rfc4122) and DCE 1.1: Authentication and Security Services. + +This package now leverages the github.com/google/uuid package (which is based off an earlier version of this package). + +###### Install +`go get github.com/pborman/uuid` + +###### Documentation +[![GoDoc](https://godoc.org/github.com/pborman/uuid?status.svg)](http://godoc.org/github.com/pborman/uuid) + +Full `go doc` style documentation for the package can be viewed online without installing this package by using the GoDoc site here: +http://godoc.org/github.com/pborman/uuid diff --git a/vendor/github.com/pborman/uuid/dce.go b/vendor/github.com/pborman/uuid/dce.go new file mode 100644 index 0000000000..50a0f2d099 --- /dev/null +++ b/vendor/github.com/pborman/uuid/dce.go @@ -0,0 +1,84 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "fmt" + "os" +) + +// A Domain represents a Version 2 domain +type Domain byte + +// Domain constants for DCE Security (Version 2) UUIDs. +const ( + Person = Domain(0) + Group = Domain(1) + Org = Domain(2) +) + +// NewDCESecurity returns a DCE Security (Version 2) UUID. +// +// The domain should be one of Person, Group or Org. +// On a POSIX system the id should be the users UID for the Person +// domain and the users GID for the Group. The meaning of id for +// the domain Org or on non-POSIX systems is site defined. +// +// For a given domain/id pair the same token may be returned for up to +// 7 minutes and 10 seconds. +func NewDCESecurity(domain Domain, id uint32) UUID { + uuid := NewUUID() + if uuid != nil { + uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2 + uuid[9] = byte(domain) + binary.BigEndian.PutUint32(uuid[0:], id) + } + return uuid +} + +// NewDCEPerson returns a DCE Security (Version 2) UUID in the person +// domain with the id returned by os.Getuid. +// +// NewDCEPerson(Person, uint32(os.Getuid())) +func NewDCEPerson() UUID { + return NewDCESecurity(Person, uint32(os.Getuid())) +} + +// NewDCEGroup returns a DCE Security (Version 2) UUID in the group +// domain with the id returned by os.Getgid. +// +// NewDCEGroup(Group, uint32(os.Getgid())) +func NewDCEGroup() UUID { + return NewDCESecurity(Group, uint32(os.Getgid())) +} + +// Domain returns the domain for a Version 2 UUID or false. +func (uuid UUID) Domain() (Domain, bool) { + if v, _ := uuid.Version(); v != 2 { + return 0, false + } + return Domain(uuid[9]), true +} + +// Id returns the id for a Version 2 UUID or false. +func (uuid UUID) Id() (uint32, bool) { + if v, _ := uuid.Version(); v != 2 { + return 0, false + } + return binary.BigEndian.Uint32(uuid[0:4]), true +} + +func (d Domain) String() string { + switch d { + case Person: + return "Person" + case Group: + return "Group" + case Org: + return "Org" + } + return fmt.Sprintf("Domain%d", int(d)) +} diff --git a/vendor/github.com/pborman/uuid/doc.go b/vendor/github.com/pborman/uuid/doc.go new file mode 100644 index 0000000000..727d761674 --- /dev/null +++ b/vendor/github.com/pborman/uuid/doc.go @@ -0,0 +1,13 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The uuid package generates and inspects UUIDs. +// +// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security +// Services. +// +// This package is a partial wrapper around the github.com/google/uuid package. +// This package represents a UUID as []byte while github.com/google/uuid +// represents a UUID as [16]byte. +package uuid diff --git a/vendor/github.com/pborman/uuid/hash.go b/vendor/github.com/pborman/uuid/hash.go new file mode 100644 index 0000000000..a0420c1ef3 --- /dev/null +++ b/vendor/github.com/pborman/uuid/hash.go @@ -0,0 +1,53 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "crypto/md5" + "crypto/sha1" + "hash" +) + +// Well known Name Space IDs and UUIDs +var ( + NameSpace_DNS = Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + NameSpace_URL = Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8") + NameSpace_OID = Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8") + NameSpace_X500 = Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8") + NIL = Parse("00000000-0000-0000-0000-000000000000") +) + +// NewHash returns a new UUID derived from the hash of space concatenated with +// data generated by h. The hash should be at least 16 byte in length. The +// first 16 bytes of the hash are used to form the UUID. The version of the +// UUID will be the lower 4 bits of version. NewHash is used to implement +// NewMD5 and NewSHA1. +func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { + h.Reset() + h.Write(space) + h.Write([]byte(data)) + s := h.Sum(nil) + uuid := make([]byte, 16) + copy(uuid, s) + uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) + uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant + return uuid +} + +// NewMD5 returns a new MD5 (Version 3) UUID based on the +// supplied name space and data. +// +// NewHash(md5.New(), space, data, 3) +func NewMD5(space UUID, data []byte) UUID { + return NewHash(md5.New(), space, data, 3) +} + +// NewSHA1 returns a new SHA1 (Version 5) UUID based on the +// supplied name space and data. +// +// NewHash(sha1.New(), space, data, 5) +func NewSHA1(space UUID, data []byte) UUID { + return NewHash(sha1.New(), space, data, 5) +} diff --git a/vendor/github.com/pborman/uuid/marshal.go b/vendor/github.com/pborman/uuid/marshal.go new file mode 100644 index 0000000000..35b89352ad --- /dev/null +++ b/vendor/github.com/pborman/uuid/marshal.go @@ -0,0 +1,85 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "errors" + "fmt" + + guuid "github.com/google/uuid" +) + +// MarshalText implements encoding.TextMarshaler. +func (u UUID) MarshalText() ([]byte, error) { + if len(u) != 16 { + return nil, nil + } + var js [36]byte + encodeHex(js[:], u) + return js[:], nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (u *UUID) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + id := Parse(string(data)) + if id == nil { + return errors.New("invalid UUID") + } + *u = id + return nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (u UUID) MarshalBinary() ([]byte, error) { + return u[:], nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (u *UUID) UnmarshalBinary(data []byte) error { + if len(data) == 0 { + return nil + } + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + var id [16]byte + copy(id[:], data) + *u = id[:] + return nil +} + +// MarshalText implements encoding.TextMarshaler. +func (u Array) MarshalText() ([]byte, error) { + var js [36]byte + encodeHex(js[:], u[:]) + return js[:], nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (u *Array) UnmarshalText(data []byte) error { + id, err := guuid.ParseBytes(data) + if err != nil { + return err + } + *u = Array(id) + return nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (u Array) MarshalBinary() ([]byte, error) { + return u[:], nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (u *Array) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(u[:], data) + return nil +} diff --git a/vendor/github.com/pborman/uuid/marshal_test.go b/vendor/github.com/pborman/uuid/marshal_test.go new file mode 100644 index 0000000000..4e85b6bab9 --- /dev/null +++ b/vendor/github.com/pborman/uuid/marshal_test.go @@ -0,0 +1,124 @@ +// Copyright 2014 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "encoding/json" + "reflect" + "testing" +) + +var testUUID = Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479") +var testArray = testUUID.Array() + +func TestJSON(t *testing.T) { + type S struct { + ID1 UUID + ID2 UUID + } + s1 := S{ID1: testUUID} + data, err := json.Marshal(&s1) + if err != nil { + t.Fatal(err) + } + var s2 S + if err := json.Unmarshal(data, &s2); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(&s1, &s2) { + t.Errorf("got %#v, want %#v", s2, s1) + } +} + +func TestJSONArray(t *testing.T) { + type S struct { + ID1 Array + ID2 Array + } + s1 := S{ID1: testArray} + data, err := json.Marshal(&s1) + if err != nil { + t.Fatal(err) + } + var s2 S + if err := json.Unmarshal(data, &s2); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(&s1, &s2) { + t.Errorf("got %#v, want %#v", s2, s1) + } +} + +func TestMarshal(t *testing.T) { + data, err := testUUID.MarshalBinary() + if err != nil { + t.Fatalf("MarhsalBinary returned unexpected error %v", err) + } + if !bytes.Equal(data, testUUID) { + t.Fatalf("MarhsalBinary returns %x, want %x", data, testUUID) + } + var u UUID + u.UnmarshalBinary(data) + if !Equal(data, u) { + t.Fatalf("UnmarhsalBinary returns %v, want %v", u, testUUID) + } +} + +func TestMarshalArray(t *testing.T) { + data, err := testArray.MarshalBinary() + if err != nil { + t.Fatalf("MarhsalBinary returned unexpected error %v", err) + } + if !bytes.Equal(data, testUUID) { + t.Fatalf("MarhsalBinary returns %x, want %x", data, testUUID) + } + var a Array + a.UnmarshalBinary(data) + if a != testArray { + t.Fatalf("UnmarhsalBinary returns %v, want %v", a, testArray) + } +} + +func TestMarshalTextArray(t *testing.T) { + data, err := testArray.MarshalText() + if err != nil { + t.Fatalf("MarhsalText returned unexpected error %v", err) + } + var a Array + a.UnmarshalText(data) + if a != testArray { + t.Fatalf("UnmarhsalText returns %v, want %v", a, testArray) + } +} + +func BenchmarkUUID_MarshalJSON(b *testing.B) { + x := &struct { + UUID UUID `json:"uuid"` + }{} + x.UUID = Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479") + if x.UUID == nil { + b.Fatal("invalid uuid") + } + for i := 0; i < b.N; i++ { + js, err := json.Marshal(x) + if err != nil { + b.Fatalf("marshal json: %#v (%v)", js, err) + } + } +} + +func BenchmarkUUID_UnmarshalJSON(b *testing.B) { + js := []byte(`{"uuid":"f47ac10b-58cc-0372-8567-0e02b2c3d479"}`) + var x *struct { + UUID UUID `json:"uuid"` + } + for i := 0; i < b.N; i++ { + err := json.Unmarshal(js, &x) + if err != nil { + b.Fatalf("marshal json: %#v (%v)", js, err) + } + } +} diff --git a/vendor/github.com/pborman/uuid/node.go b/vendor/github.com/pborman/uuid/node.go new file mode 100644 index 0000000000..e524e0101b --- /dev/null +++ b/vendor/github.com/pborman/uuid/node.go @@ -0,0 +1,50 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + guuid "github.com/google/uuid" +) + +// NodeInterface returns the name of the interface from which the NodeID was +// derived. The interface "user" is returned if the NodeID was set by +// SetNodeID. +func NodeInterface() string { + return guuid.NodeInterface() +} + +// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs. +// If name is "" then the first usable interface found will be used or a random +// Node ID will be generated. If a named interface cannot be found then false +// is returned. +// +// SetNodeInterface never fails when name is "". +func SetNodeInterface(name string) bool { + return guuid.SetNodeInterface(name) +} + +// NodeID returns a slice of a copy of the current Node ID, setting the Node ID +// if not already set. +func NodeID() []byte { + return guuid.NodeID() +} + +// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes +// of id are used. If id is less than 6 bytes then false is returned and the +// Node ID is not set. +func SetNodeID(id []byte) bool { + return guuid.SetNodeID(id) +} + +// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is +// not valid. The NodeID is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) NodeID() []byte { + if len(uuid) != 16 { + return nil + } + node := make([]byte, 6) + copy(node, uuid[10:]) + return node +} diff --git a/vendor/github.com/pborman/uuid/seq_test.go b/vendor/github.com/pborman/uuid/seq_test.go new file mode 100644 index 0000000000..3b3d1430d5 --- /dev/null +++ b/vendor/github.com/pborman/uuid/seq_test.go @@ -0,0 +1,66 @@ +// Copyright 2014 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "flag" + "runtime" + "testing" + "time" +) + +// This test is only run when --regressions is passed on the go test line. +var regressions = flag.Bool("regressions", false, "run uuid regression tests") + +// TestClockSeqRace tests for a particular race condition of returning two +// identical Version1 UUIDs. The duration of 1 minute was chosen as the race +// condition, before being fixed, nearly always occured in under 30 seconds. +func TestClockSeqRace(t *testing.T) { + if !*regressions { + t.Skip("skipping regression tests") + } + duration := time.Minute + + done := make(chan struct{}) + defer close(done) + + ch := make(chan UUID, 10000) + ncpu := runtime.NumCPU() + switch ncpu { + case 0, 1: + // We can't run the test effectively. + t.Skip("skipping race test, only one CPU detected") + return + default: + runtime.GOMAXPROCS(ncpu) + } + for i := 0; i < ncpu; i++ { + go func() { + for { + select { + case <-done: + return + case ch <- NewUUID(): + } + } + }() + } + + uuids := make(map[string]bool) + cnt := 0 + start := time.Now() + for u := range ch { + s := u.String() + if uuids[s] { + t.Errorf("duplicate uuid after %d in %v: %s", cnt, time.Since(start), s) + return + } + uuids[s] = true + if time.Since(start) > duration { + return + } + cnt++ + } +} diff --git a/vendor/github.com/pborman/uuid/sql.go b/vendor/github.com/pborman/uuid/sql.go new file mode 100644 index 0000000000..929c3847e2 --- /dev/null +++ b/vendor/github.com/pborman/uuid/sql.go @@ -0,0 +1,68 @@ +// Copyright 2015 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "database/sql/driver" + "errors" + "fmt" +) + +// Scan implements sql.Scanner so UUIDs can be read from databases transparently +// Currently, database types that map to string and []byte are supported. Please +// consult database-specific driver documentation for matching types. +func (uuid *UUID) Scan(src interface{}) error { + switch src.(type) { + case string: + // if an empty UUID comes from a table, we return a null UUID + if src.(string) == "" { + return nil + } + + // see uuid.Parse for required string format + parsed := Parse(src.(string)) + + if parsed == nil { + return errors.New("Scan: invalid UUID format") + } + + *uuid = parsed + case []byte: + b := src.([]byte) + + // if an empty UUID comes from a table, we return a null UUID + if len(b) == 0 { + return nil + } + + // assumes a simple slice of bytes if 16 bytes + // otherwise attempts to parse + if len(b) == 16 { + parsed := make([]byte, 16) + copy(parsed, b) + *uuid = UUID(parsed) + } else { + u := Parse(string(b)) + + if u == nil { + return errors.New("Scan: invalid UUID format") + } + + *uuid = u + } + + default: + return fmt.Errorf("Scan: unable to scan type %T into UUID", src) + } + + return nil +} + +// Value implements sql.Valuer so that UUIDs can be written to databases +// transparently. Currently, UUIDs map to strings. Please consult +// database-specific driver documentation for matching types. +func (uuid UUID) Value() (driver.Value, error) { + return uuid.String(), nil +} diff --git a/vendor/github.com/pborman/uuid/sql_test.go b/vendor/github.com/pborman/uuid/sql_test.go new file mode 100644 index 0000000000..1030951569 --- /dev/null +++ b/vendor/github.com/pborman/uuid/sql_test.go @@ -0,0 +1,96 @@ +// Copyright 2015 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "strings" + "testing" +) + +func TestScan(t *testing.T) { + var stringTest string = "f47ac10b-58cc-0372-8567-0e02b2c3d479" + var byteTest []byte = Parse(stringTest) + var badTypeTest int = 6 + var invalidTest string = "f47ac10b-58cc-0372-8567-0e02b2c3d4" + + // sunny day tests + + var uuid UUID + err := (&uuid).Scan(stringTest) + if err != nil { + t.Fatal(err) + } + + err = (&uuid).Scan([]byte(stringTest)) + if err != nil { + t.Fatal(err) + } + + err = (&uuid).Scan(byteTest) + if err != nil { + t.Fatal(err) + } + + // bad type tests + + err = (&uuid).Scan(badTypeTest) + if err == nil { + t.Error("int correctly parsed and shouldn't have") + } + if !strings.Contains(err.Error(), "unable to scan type") { + t.Error("attempting to parse an int returned an incorrect error message") + } + + // invalid/incomplete uuids + + err = (&uuid).Scan(invalidTest) + if err == nil { + t.Error("invalid uuid was parsed without error") + } + if !strings.Contains(err.Error(), "invalid UUID") { + t.Error("attempting to parse an invalid UUID returned an incorrect error message") + } + + err = (&uuid).Scan(byteTest[:len(byteTest)-2]) + if err == nil { + t.Error("invalid byte uuid was parsed without error") + } + if !strings.Contains(err.Error(), "invalid UUID") { + t.Error("attempting to parse an invalid byte UUID returned an incorrect error message") + } + + // empty tests + + uuid = nil + var emptySlice []byte + err = (&uuid).Scan(emptySlice) + if err != nil { + t.Fatal(err) + } + + if uuid != nil { + t.Error("UUID was not nil after scanning empty byte slice") + } + + uuid = nil + var emptyString string + err = (&uuid).Scan(emptyString) + if err != nil { + t.Fatal(err) + } + + if uuid != nil { + t.Error("UUID was not nil after scanning empty string") + } +} + +func TestValue(t *testing.T) { + stringTest := "f47ac10b-58cc-0372-8567-0e02b2c3d479" + uuid := Parse(stringTest) + val, _ := uuid.Value() + if val != stringTest { + t.Error("Value() did not return expected string") + } +} diff --git a/vendor/github.com/pborman/uuid/time.go b/vendor/github.com/pborman/uuid/time.go new file mode 100644 index 0000000000..5c0960d872 --- /dev/null +++ b/vendor/github.com/pborman/uuid/time.go @@ -0,0 +1,57 @@ +// Copyright 2014 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + + guuid "github.com/google/uuid" +) + +// A Time represents a time as the number of 100's of nanoseconds since 15 Oct +// 1582. +type Time = guuid.Time + +// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and +// clock sequence as well as adjusting the clock sequence as needed. An error +// is returned if the current time cannot be determined. +func GetTime() (Time, uint16, error) { return guuid.GetTime() } + +// ClockSequence returns the current clock sequence, generating one if not +// already set. The clock sequence is only used for Version 1 UUIDs. +// +// The uuid package does not use global static storage for the clock sequence or +// the last time a UUID was generated. Unless SetClockSequence a new random +// clock sequence is generated the first time a clock sequence is requested by +// ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) sequence is generated +// for +func ClockSequence() int { return guuid.ClockSequence() } + +// SetClockSeq sets the clock sequence to the lower 14 bits of seq. Setting to +// -1 causes a new sequence to be generated. +func SetClockSequence(seq int) { guuid.SetClockSequence(seq) } + +// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in +// uuid. It returns false if uuid is not valid. The time is only well defined +// for version 1 and 2 UUIDs. +func (uuid UUID) Time() (Time, bool) { + if len(uuid) != 16 { + return 0, false + } + time := int64(binary.BigEndian.Uint32(uuid[0:4])) + time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32 + time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48 + return Time(time), true +} + +// ClockSequence returns the clock sequence encoded in uuid. It returns false +// if uuid is not valid. The clock sequence is only well defined for version 1 +// and 2 UUIDs. +func (uuid UUID) ClockSequence() (int, bool) { + if len(uuid) != 16 { + return 0, false + } + return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff, true +} diff --git a/vendor/github.com/pborman/uuid/util.go b/vendor/github.com/pborman/uuid/util.go new file mode 100644 index 0000000000..255b5e2485 --- /dev/null +++ b/vendor/github.com/pborman/uuid/util.go @@ -0,0 +1,32 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +// xvalues returns the value of a byte as a hexadecimal digit or 255. +var xvalues = [256]byte{ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +} + +// xtob converts the the first two hex bytes of x into a byte. +func xtob(x string) (byte, bool) { + b1 := xvalues[x[0]] + b2 := xvalues[x[1]] + return (b1 << 4) | b2, b1 != 255 && b2 != 255 +} diff --git a/vendor/github.com/pborman/uuid/uuid.go b/vendor/github.com/pborman/uuid/uuid.go new file mode 100644 index 0000000000..a2429b0dd0 --- /dev/null +++ b/vendor/github.com/pborman/uuid/uuid.go @@ -0,0 +1,163 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "io" + + guuid "github.com/google/uuid" +) + +// Array is a pass-by-value UUID that can be used as an effecient key in a map. +type Array [16]byte + +// UUID converts uuid into a slice. +func (uuid Array) UUID() UUID { + return uuid[:] +} + +// String returns the string representation of uuid, +// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. +func (uuid Array) String() string { + return guuid.UUID(uuid).String() +} + +// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC +// 4122. +type UUID []byte + +// A Version represents a UUIDs version. +type Version = guuid.Version + +// A Variant represents a UUIDs variant. +type Variant = guuid.Variant + +// Constants returned by Variant. +const ( + Invalid = guuid.Invalid // Invalid UUID + RFC4122 = guuid.RFC4122 // The variant specified in RFC4122 + Reserved = guuid.Reserved // Reserved, NCS backward compatibility. + Microsoft = guuid.Microsoft // Reserved, Microsoft Corporation backward compatibility. + Future = guuid.Future // Reserved for future definition. +) + +var rander = rand.Reader // random function + +// New returns a new random (version 4) UUID as a string. It is a convenience +// function for NewRandom().String(). +func New() string { + return NewRandom().String() +} + +// Parse decodes s into a UUID or returns nil. Both the UUID form of +// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded. +func Parse(s string) UUID { + gu, err := guuid.Parse(s) + if err == nil { + return gu[:] + } + return nil +} + +// ParseBytes is like Parse, except it parses a byte slice instead of a string. +func ParseBytes(b []byte) (UUID, error) { + gu, err := guuid.ParseBytes(b) + if err == nil { + return gu[:], nil + } + return nil, err +} + +// Equal returns true if uuid1 and uuid2 are equal. +func Equal(uuid1, uuid2 UUID) bool { + return bytes.Equal(uuid1, uuid2) +} + +// Array returns an array representation of uuid that can be used as a map key. +// Array panics if uuid is not valid. +func (uuid UUID) Array() Array { + if len(uuid) != 16 { + panic("invalid uuid") + } + var a Array + copy(a[:], uuid) + return a +} + +// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// , or "" if uuid is invalid. +func (uuid UUID) String() string { + if len(uuid) != 16 { + return "" + } + var buf [36]byte + encodeHex(buf[:], uuid) + return string(buf[:]) +} + +// URN returns the RFC 2141 URN form of uuid, +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid. +func (uuid UUID) URN() string { + if len(uuid) != 16 { + return "" + } + var buf [36 + 9]byte + copy(buf[:], "urn:uuid:") + encodeHex(buf[9:], uuid) + return string(buf[:]) +} + +func encodeHex(dst []byte, uuid UUID) { + hex.Encode(dst[:], uuid[:4]) + dst[8] = '-' + hex.Encode(dst[9:13], uuid[4:6]) + dst[13] = '-' + hex.Encode(dst[14:18], uuid[6:8]) + dst[18] = '-' + hex.Encode(dst[19:23], uuid[8:10]) + dst[23] = '-' + hex.Encode(dst[24:], uuid[10:]) +} + +// Variant returns the variant encoded in uuid. It returns Invalid if +// uuid is invalid. +func (uuid UUID) Variant() Variant { + if len(uuid) != 16 { + return Invalid + } + switch { + case (uuid[8] & 0xc0) == 0x80: + return RFC4122 + case (uuid[8] & 0xe0) == 0xc0: + return Microsoft + case (uuid[8] & 0xe0) == 0xe0: + return Future + default: + return Reserved + } +} + +// Version returns the version of uuid. It returns false if uuid is not +// valid. +func (uuid UUID) Version() (Version, bool) { + if len(uuid) != 16 { + return 0, false + } + return Version(uuid[6] >> 4), true +} + +// SetRand sets the random number generator to r, which implements io.Reader. +// If r.Read returns an error when the package requests random data then +// a panic will be issued. +// +// Calling SetRand with nil sets the random number generator to the default +// generator. +func SetRand(r io.Reader) { + guuid.SetRand(r) +} diff --git a/vendor/github.com/pborman/uuid/uuid_test.go b/vendor/github.com/pborman/uuid/uuid_test.go new file mode 100644 index 0000000000..7e9d144fba --- /dev/null +++ b/vendor/github.com/pborman/uuid/uuid_test.go @@ -0,0 +1,410 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +// Some of these tests can probably be removed as they are redundant with the +// tests in github.com/google/uuid. + +import ( + "bytes" + "fmt" + "os" + "strings" + "testing" +) + +type test struct { + in string + version Version + variant Variant + isuuid bool +} + +var tests = []test{ + {"f47ac10b-58cc-0372-8567-0e02b2c3d479", 0, RFC4122, true}, + {"f47ac10b-58cc-1372-8567-0e02b2c3d479", 1, RFC4122, true}, + {"f47ac10b-58cc-2372-8567-0e02b2c3d479", 2, RFC4122, true}, + {"f47ac10b-58cc-3372-8567-0e02b2c3d479", 3, RFC4122, true}, + {"f47ac10b-58cc-4372-8567-0e02b2c3d479", 4, RFC4122, true}, + {"f47ac10b-58cc-5372-8567-0e02b2c3d479", 5, RFC4122, true}, + {"f47ac10b-58cc-6372-8567-0e02b2c3d479", 6, RFC4122, true}, + {"f47ac10b-58cc-7372-8567-0e02b2c3d479", 7, RFC4122, true}, + {"f47ac10b-58cc-8372-8567-0e02b2c3d479", 8, RFC4122, true}, + {"f47ac10b-58cc-9372-8567-0e02b2c3d479", 9, RFC4122, true}, + {"f47ac10b-58cc-a372-8567-0e02b2c3d479", 10, RFC4122, true}, + {"f47ac10b-58cc-b372-8567-0e02b2c3d479", 11, RFC4122, true}, + {"f47ac10b-58cc-c372-8567-0e02b2c3d479", 12, RFC4122, true}, + {"f47ac10b-58cc-d372-8567-0e02b2c3d479", 13, RFC4122, true}, + {"f47ac10b-58cc-e372-8567-0e02b2c3d479", 14, RFC4122, true}, + {"f47ac10b-58cc-f372-8567-0e02b2c3d479", 15, RFC4122, true}, + + {"urn:uuid:f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true}, + {"URN:UUID:f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-1567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-2567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-3567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-4567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-5567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-6567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-7567-0e02b2c3d479", 4, Reserved, true}, + {"f47ac10b-58cc-4372-8567-0e02b2c3d479", 4, RFC4122, true}, + {"f47ac10b-58cc-4372-9567-0e02b2c3d479", 4, RFC4122, true}, + {"f47ac10b-58cc-4372-a567-0e02b2c3d479", 4, RFC4122, true}, + {"f47ac10b-58cc-4372-b567-0e02b2c3d479", 4, RFC4122, true}, + {"f47ac10b-58cc-4372-c567-0e02b2c3d479", 4, Microsoft, true}, + {"f47ac10b-58cc-4372-d567-0e02b2c3d479", 4, Microsoft, true}, + {"f47ac10b-58cc-4372-e567-0e02b2c3d479", 4, Future, true}, + {"f47ac10b-58cc-4372-f567-0e02b2c3d479", 4, Future, true}, + + {"f47ac10b158cc-5372-a567-0e02b2c3d479", 0, Invalid, false}, + {"f47ac10b-58cc25372-a567-0e02b2c3d479", 0, Invalid, false}, + {"f47ac10b-58cc-53723a567-0e02b2c3d479", 0, Invalid, false}, + {"f47ac10b-58cc-5372-a56740e02b2c3d479", 0, Invalid, false}, + {"f47ac10b-58cc-5372-a567-0e02-2c3d479", 0, Invalid, false}, + {"g47ac10b-58cc-4372-a567-0e02b2c3d479", 0, Invalid, false}, +} + +var constants = []struct { + c interface{} + name string +}{ + {Person, "Person"}, + {Group, "Group"}, + {Org, "Org"}, + {Invalid, "Invalid"}, + {RFC4122, "RFC4122"}, + {Reserved, "Reserved"}, + {Microsoft, "Microsoft"}, + {Future, "Future"}, + {Domain(17), "Domain17"}, + {Variant(42), "BadVariant42"}, +} + +func testTest(t *testing.T, in string, tt test) { + uuid := Parse(in) + if ok := (uuid != nil); ok != tt.isuuid { + t.Errorf("Parse(%s) got %v expected %v\b", in, ok, tt.isuuid) + } + if uuid == nil { + return + } + + if v := uuid.Variant(); v != tt.variant { + t.Errorf("Variant(%s) got %d expected %d\b", in, v, tt.variant) + } + if v, _ := uuid.Version(); v != tt.version { + t.Errorf("Version(%s) got %d expected %d\b", in, v, tt.version) + } +} + +func TestUUID(t *testing.T) { + for _, tt := range tests { + testTest(t, tt.in, tt) + testTest(t, strings.ToUpper(tt.in), tt) + } +} + +func TestConstants(t *testing.T) { + for x, tt := range constants { + v, ok := tt.c.(fmt.Stringer) + if !ok { + t.Errorf("%x: %v: not a stringer", x, v) + } else if s := v.String(); s != tt.name { + v, _ := tt.c.(int) + t.Errorf("%x: Constant %T:%d gives %q, expected %q", x, tt.c, v, s, tt.name) + } + } +} + +func TestRandomUUID(t *testing.T) { + m := make(map[string]bool) + for x := 1; x < 32; x++ { + uuid := NewRandom() + s := uuid.String() + if m[s] { + t.Errorf("NewRandom returned duplicated UUID %s", s) + } + m[s] = true + if v, _ := uuid.Version(); v != 4 { + t.Errorf("Random UUID of version %s", v) + } + if uuid.Variant() != RFC4122 { + t.Errorf("Random UUID is variant %d", uuid.Variant()) + } + } +} + +func TestNew(t *testing.T) { + m := make(map[string]bool) + for x := 1; x < 32; x++ { + s := New() + if m[s] { + t.Errorf("New returned duplicated UUID %s", s) + } + m[s] = true + uuid := Parse(s) + if uuid == nil { + t.Errorf("New returned %q which does not decode", s) + continue + } + if v, _ := uuid.Version(); v != 4 { + t.Errorf("Random UUID of version %s", v) + } + if uuid.Variant() != RFC4122 { + t.Errorf("Random UUID is variant %d", uuid.Variant()) + } + } +} + +func TestCoding(t *testing.T) { + text := "7d444840-9dc0-11d1-b245-5ffdce74fad2" + urn := "urn:uuid:7d444840-9dc0-11d1-b245-5ffdce74fad2" + data := UUID{ + 0x7d, 0x44, 0x48, 0x40, + 0x9d, 0xc0, + 0x11, 0xd1, + 0xb2, 0x45, + 0x5f, 0xfd, 0xce, 0x74, 0xfa, 0xd2, + } + if v := data.String(); v != text { + t.Errorf("%x: encoded to %s, expected %s", data, v, text) + } + if v := data.URN(); v != urn { + t.Errorf("%x: urn is %s, expected %s", data, v, urn) + } + + uuid := Parse(text) + if !Equal(uuid, data) { + t.Errorf("%s: decoded to %s, expected %s", text, uuid, data) + } +} + +func TestVersion1(t *testing.T) { + uuid1 := NewUUID() + uuid2 := NewUUID() + + if Equal(uuid1, uuid2) { + t.Errorf("%s:duplicate uuid", uuid1) + } + if v, _ := uuid1.Version(); v != 1 { + t.Errorf("%s: version %s expected 1", uuid1, v) + } + if v, _ := uuid2.Version(); v != 1 { + t.Errorf("%s: version %s expected 1", uuid2, v) + } + n1 := uuid1.NodeID() + n2 := uuid2.NodeID() + if !bytes.Equal(n1, n2) { + t.Errorf("Different nodes %x != %x", n1, n2) + } + t1, ok := uuid1.Time() + if !ok { + t.Errorf("%s: invalid time", uuid1) + } + t2, ok := uuid2.Time() + if !ok { + t.Errorf("%s: invalid time", uuid2) + } + q1, ok := uuid1.ClockSequence() + if !ok { + t.Errorf("%s: invalid clock sequence", uuid1) + } + q2, ok := uuid2.ClockSequence() + if !ok { + t.Errorf("%s: invalid clock sequence", uuid2) + } + + switch { + case t1 == t2 && q1 == q2: + t.Error("time stopped") + case t1 > t2 && q1 == q2: + t.Error("time reversed") + case t1 < t2 && q1 != q2: + t.Error("clock sequence chaned unexpectedly") + } +} + +func TestMD5(t *testing.T) { + uuid := NewMD5(NameSpace_DNS, []byte("python.org")).String() + want := "6fa459ea-ee8a-3ca4-894e-db77e160355e" + if uuid != want { + t.Errorf("MD5: got %q expected %q", uuid, want) + } +} + +func TestSHA1(t *testing.T) { + uuid := NewSHA1(NameSpace_DNS, []byte("python.org")).String() + want := "886313e1-3b8a-5372-9b90-0c9aee199e5d" + if uuid != want { + t.Errorf("SHA1: got %q expected %q", uuid, want) + } +} + +func testDCE(t *testing.T, name string, uuid UUID, domain Domain, id uint32) { + if uuid == nil { + t.Errorf("%s failed", name) + return + } + if v, _ := uuid.Version(); v != 2 { + t.Errorf("%s: %s: expected version 2, got %s", name, uuid, v) + return + } + if v, ok := uuid.Domain(); !ok || v != domain { + if !ok { + t.Errorf("%s: %d: Domain failed", name, uuid) + } else { + t.Errorf("%s: %s: expected domain %d, got %d", name, uuid, domain, v) + } + } + if v, ok := uuid.Id(); !ok || v != id { + if !ok { + t.Errorf("%s: %d: Id failed", name, uuid) + } else { + t.Errorf("%s: %s: expected id %d, got %d", name, uuid, id, v) + } + } +} + +func TestDCE(t *testing.T) { + testDCE(t, "NewDCESecurity", NewDCESecurity(42, 12345678), 42, 12345678) + testDCE(t, "NewDCEPerson", NewDCEPerson(), Person, uint32(os.Getuid())) + testDCE(t, "NewDCEGroup", NewDCEGroup(), Group, uint32(os.Getgid())) +} + +type badRand struct{} + +func (r badRand) Read(buf []byte) (int, error) { + for i, _ := range buf { + buf[i] = byte(i) + } + return len(buf), nil +} + +func TestBadRand(t *testing.T) { + SetRand(badRand{}) + uuid1 := New() + uuid2 := New() + if uuid1 != uuid2 { + t.Errorf("expected duplicates, got %q and %q", uuid1, uuid2) + } + SetRand(nil) + uuid1 = New() + uuid2 = New() + if uuid1 == uuid2 { + t.Errorf("unexpected duplicates, got %q", uuid1) + } +} + +func TestUUID_Array(t *testing.T) { + expect := Array{ + 0xf4, 0x7a, 0xc1, 0x0b, + 0x58, 0xcc, + 0x03, 0x72, + 0x85, 0x67, + 0x0e, 0x02, 0xb2, 0xc3, 0xd4, 0x79, + } + uuid := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479") + if uuid == nil { + t.Fatal("invalid uuid") + } + if uuid.Array() != expect { + t.Fatal("invalid array") + } +} + +func TestArray_UUID(t *testing.T) { + array := Array{ + 0xf4, 0x7a, 0xc1, 0x0b, + 0x58, 0xcc, + 0x03, 0x72, + 0x85, 0x67, + 0x0e, 0x02, 0xb2, 0xc3, 0xd4, 0x79, + } + expect := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479") + if expect == nil { + t.Fatal("invalid uuid") + } + if !bytes.Equal(array.UUID(), expect) { + t.Fatal("invalid uuid") + } +} + +func BenchmarkParse(b *testing.B) { + for i := 0; i < b.N; i++ { + uuid := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479") + if uuid == nil { + b.Fatal("invalid uuid") + } + } +} + +func BenchmarkNew(b *testing.B) { + for i := 0; i < b.N; i++ { + New() + } +} + +func BenchmarkUUID_String(b *testing.B) { + uuid := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479") + if uuid == nil { + b.Fatal("invalid uuid") + } + for i := 0; i < b.N; i++ { + if uuid.String() == "" { + b.Fatal("invalid uuid") + } + } +} + +func BenchmarkUUID_URN(b *testing.B) { + uuid := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479") + if uuid == nil { + b.Fatal("invalid uuid") + } + for i := 0; i < b.N; i++ { + if uuid.URN() == "" { + b.Fatal("invalid uuid") + } + } +} + +func BenchmarkUUID_Array(b *testing.B) { + expect := Array{ + 0xf4, 0x7a, 0xc1, 0x0b, + 0x58, 0xcc, + 0x03, 0x72, + 0x85, 0x67, + 0x0e, 0x02, 0xb2, 0xc3, 0xd4, 0x79, + } + uuid := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479") + if uuid == nil { + b.Fatal("invalid uuid") + } + for i := 0; i < b.N; i++ { + if uuid.Array() != expect { + b.Fatal("invalid array") + } + } +} + +func BenchmarkArray_UUID(b *testing.B) { + array := Array{ + 0xf4, 0x7a, 0xc1, 0x0b, + 0x58, 0xcc, + 0x03, 0x72, + 0x85, 0x67, + 0x0e, 0x02, 0xb2, 0xc3, 0xd4, 0x79, + } + expect := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479") + if expect == nil { + b.Fatal("invalid uuid") + } + for i := 0; i < b.N; i++ { + if !bytes.Equal(array.UUID(), expect) { + b.Fatal("invalid uuid") + } + } +} diff --git a/vendor/github.com/pborman/uuid/version1.go b/vendor/github.com/pborman/uuid/version1.go new file mode 100644 index 0000000000..7af948da79 --- /dev/null +++ b/vendor/github.com/pborman/uuid/version1.go @@ -0,0 +1,23 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + guuid "github.com/google/uuid" +) + +// NewUUID returns a Version 1 UUID based on the current NodeID and clock +// sequence, and the current time. If the NodeID has not been set by SetNodeID +// or SetNodeInterface then it will be set automatically. If the NodeID cannot +// be set NewUUID returns nil. If clock sequence has not been set by +// SetClockSequence then it will be set automatically. If GetTime fails to +// return the current NewUUID returns nil. +func NewUUID() UUID { + gu, err := guuid.NewUUID() + if err == nil { + return UUID(gu[:]) + } + return nil +} diff --git a/vendor/github.com/pborman/uuid/version4.go b/vendor/github.com/pborman/uuid/version4.go new file mode 100644 index 0000000000..b459d46d13 --- /dev/null +++ b/vendor/github.com/pborman/uuid/version4.go @@ -0,0 +1,26 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import guuid "github.com/google/uuid" + +// Random returns a Random (Version 4) UUID or panics. +// +// The strength of the UUIDs is based on the strength of the crypto/rand +// package. +// +// A note about uniqueness derived from the UUID Wikipedia entry: +// +// Randomly generated UUIDs have 122 random bits. One's annual risk of being +// hit by a meteorite is estimated to be one chance in 17 billion, that +// means the probability is about 0.00000000006 (6 × 10−11), +// equivalent to the odds of creating a few tens of trillions of UUIDs in a +// year and having one duplicate. +func NewRandom() UUID { + if gu, err := guuid.NewRandom(); err == nil { + return UUID(gu[:]) + } + return nil +} diff --git a/vendor/github.com/prometheus/client_golang/.gitignore b/vendor/github.com/prometheus/client_golang/.gitignore index f6fc2e8eb2..5725b80fd9 100644 --- a/vendor/github.com/prometheus/client_golang/.gitignore +++ b/vendor/github.com/prometheus/client_golang/.gitignore @@ -7,6 +7,10 @@ _obj _test +# Examples +/examples/simple/simple +/examples/random/random + # Architecture specific extensions/prefixes *.[568vq] [568vq].out diff --git a/vendor/github.com/prometheus/client_golang/AUTHORS.md b/vendor/github.com/prometheus/client_golang/AUTHORS.md deleted file mode 100644 index c5275d5ab1..0000000000 --- a/vendor/github.com/prometheus/client_golang/AUTHORS.md +++ /dev/null @@ -1,18 +0,0 @@ -The Prometheus project was started by Matt T. Proud (emeritus) and -Julius Volz in 2012. - -Maintainers of this repository: - -* Björn Rabenstein - -The following individuals have contributed code to this repository -(listed in alphabetical order): - -* Bernerd Schaefer -* Björn Rabenstein -* Daniel Bornkessel -* Jeff Younker -* Julius Volz -* Matt T. Proud -* Tobias Schmidt - diff --git a/vendor/github.com/prometheus/client_golang/CHANGELOG.md b/vendor/github.com/prometheus/client_golang/CHANGELOG.md index 330788a4ee..fcfa112d6a 100644 --- a/vendor/github.com/prometheus/client_golang/CHANGELOG.md +++ b/vendor/github.com/prometheus/client_golang/CHANGELOG.md @@ -1,3 +1,75 @@ +## 0.9.2 / 2018-12-06 +* [FEATURE] Support for Go modules. #501 +* [FEATURE] `Timer.ObserveDuration` returns observed duration. #509 +* [ENHANCEMENT] Improved doc comments and error messages. #504 +* [BUGFIX] Fix race condition during metrics gathering. #512 +* [BUGFIX] Fix testutil metric comparison for Histograms and empty labels. #494 + #498 + +## 0.9.1 / 2018-11-03 +* [FEATURE] Add `WriteToTextfile` function to facilitate the creation of + *.prom files for the textfile collector of the node exporter. #489 +* [ENHANCEMENT] More descriptive error messages for inconsistent label + cardinality. #487 +* [ENHANCEMENT] Exposition: Use a GZIP encoder pool to avoid allocations in + high-frequency scrape scenarios. #366 +* [ENHANCEMENT] Exposition: Streaming serving of metrics data while encoding. + #482 +* [ENHANCEMENT] API client: Add a way to return the body of a 5xx response. + #479 + +## 0.9.0 / 2018-10-15 +* [CHANGE] Go1.6 is no longer supported. +* [CHANGE] More refinements of the `Registry` consistency checks: Duplicated + labels are now detected, but inconsistent label dimensions are now allowed. + Collisions with the “magic” metric and label names in Summaries and + Histograms are detected now. #108 #417 #471 +* [CHANGE] Changed `ProcessCollector` constructor. #219 +* [CHANGE] Changed Go counter `go_memstats_heap_released_bytes_total` to gauge + `go_memstats_heap_released_bytes`. #229 +* [CHANGE] Unexported `LabelPairSorter`. #453 +* [CHANGE] Removed the `Untyped` metric from direct instrumentation. #340 +* [CHANGE] Unexported `MetricVec`. #319 +* [CHANGE] Removed deprecated `Set` method from `Counter` #247 +* [CHANGE] Removed deprecated `RegisterOrGet` and `MustRegisterOrGet`. #247 +* [CHANGE] API client: Introduced versioned packages. +* [FEATURE] A `Registerer` can be wrapped with prefixes and labels. #357 +* [FEATURE] “Describe by collect” helper function. #239 +* [FEATURE] Added package `testutil`. #58 +* [FEATURE] Timestamp can be explicitly set for const metrics. #187 +* [FEATURE] “Unchecked” collectors are possible now without cheating. #47 +* [FEATURE] Pushing to the Pushgateway reworked in package `push` to support + many new features. (The old functions are still usable but deprecated.) #372 + #341 +* [FEATURE] Configurable connection limit for scrapes. #179 +* [FEATURE] New HTTP middlewares to instrument `http.Handler` and + `http.RoundTripper`. The old middlewares and the pre-instrumented `/metrics` + handler are (strongly) deprecated. #316 #57 #101 #224 +* [FEATURE] “Currying” for metric vectors. #320 +* [FEATURE] A `Summary` can be created without quantiles. #118 +* [FEATURE] Added a `Timer` helper type. #231 +* [FEATURE] Added a Graphite bridge. #197 +* [FEATURE] Help strings are now optional. #460 +* [FEATURE] Added `process_virtual_memory_max_bytes` metric. #438 #440 +* [FEATURE] Added `go_gc_cpu_fraction` and `go_threads` metrics. #281 #277 +* [FEATURE] Added `promauto` package with auto-registering metrics. #385 #393 +* [FEATURE] Add `SetToCurrentTime` method to `Gauge`. #259 +* [FEATURE] API client: Add AlertManager, Status, and Target methods. #402 +* [FEATURE] API client: Add admin methods. #398 +* [FEATURE] API client: Support series API. #361 +* [FEATURE] API client: Support querying label values. +* [ENHANCEMENT] Smarter creation of goroutines during scraping. Solves memory + usage spikes in certain situations. #369 +* [ENHANCEMENT] Counters are now faster if dealing with integers only. #367 +* [ENHANCEMENT] Improved label validation. #274 #335 +* [BUGFIX] Creating a const metric with an invalid `Desc` returns an error. #460 +* [BUGFIX] Histogram observations don't race any longer with exposition. #275 +* [BUGFIX] Fixed goroutine leaks. #236 #472 +* [BUGFIX] Fixed an error message for exponential histogram buckets. #467 +* [BUGFIX] Fixed data race writing to the metric map. #401 +* [BUGFIX] API client: Decode JSON on a 4xx respons but do not on 204 + responses. #476 #414 + ## 0.8.0 / 2016-08-17 * [CHANGE] Registry is doing more consistency checks. This might break existing setups that used to export inconsistent metrics. diff --git a/vendor/github.com/prometheus/client_golang/CONTRIBUTING.md b/vendor/github.com/prometheus/client_golang/CONTRIBUTING.md index 5705f0fbea..e015a85cbb 100644 --- a/vendor/github.com/prometheus/client_golang/CONTRIBUTING.md +++ b/vendor/github.com/prometheus/client_golang/CONTRIBUTING.md @@ -2,9 +2,9 @@ Prometheus uses GitHub to manage reviews of pull requests. -* If you have a trivial fix or improvement, go ahead and create a pull - request, addressing (with `@...`) one or more of the maintainers - (see [AUTHORS.md](AUTHORS.md)) in the description of the pull request. +* If you have a trivial fix or improvement, go ahead and create a pull request, + addressing (with `@...`) the maintainer of this repository (see + [MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request. * If you plan to do something more involved, first discuss your ideas on our [mailing list](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers). @@ -16,3 +16,5 @@ Prometheus uses GitHub to manage reviews of pull requests. and the _Formatting and style_ section of Peter Bourgon's [Go: Best Practices for Production Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style). + +* Be sure to sign off on the [DCO](https://github.com/probot/dco#how-it-works) diff --git a/vendor/github.com/prometheus/client_golang/Dockerfile b/vendor/github.com/prometheus/client_golang/Dockerfile new file mode 100644 index 0000000000..211f22b9db --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/Dockerfile @@ -0,0 +1,23 @@ +# This Dockerfile builds an image for a client_golang example. +# +# Use as (from the root for the client_golang repository): +# docker build -f examples/$name/Dockerfile -t prometheus/golang-example-$name . + +# Builder image, where we build the example. +FROM golang:1 AS builder +WORKDIR /go/src/github.com/prometheus/client_golang +COPY . . +WORKDIR /go/src/github.com/prometheus/client_golang/prometheus +RUN go get -d +WORKDIR /go/src/github.com/prometheus/client_golang/examples/random +RUN CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags '-w' +WORKDIR /go/src/github.com/prometheus/client_golang/examples/simple +RUN CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags '-w' + +# Final image. +FROM prom/busybox +LABEL maintainer="The Prometheus Authors " +COPY --from=builder /go/src/github.com/prometheus/client_golang/examples/random \ + /go/src/github.com/prometheus/client_golang/examples/simple ./ +EXPOSE 8080 +CMD echo Please run an example. Either /random or /simple diff --git a/vendor/github.com/prometheus/client_golang/MAINTAINERS.md b/vendor/github.com/prometheus/client_golang/MAINTAINERS.md new file mode 100644 index 0000000000..66668ad012 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/MAINTAINERS.md @@ -0,0 +1,2 @@ +* Krasi Georgiev for `api/...` +* Björn Rabenstein for everything else diff --git a/vendor/github.com/prometheus/client_golang/Makefile b/vendor/github.com/prometheus/client_golang/Makefile new file mode 100644 index 0000000000..443c360b92 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/Makefile @@ -0,0 +1,32 @@ +# Copyright 2018 The Prometheus Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +include Makefile.common + +# http.CloseNotifier is deprecated but we don't want to remove support +# from client_golang to not break anybody still using it. +STATICCHECK_IGNORE = \ + github.com/prometheus/client_golang/prometheus/promhttp/delegator*.go:SA1019 \ + github.com/prometheus/client_golang/prometheus/promhttp/instrument_server_test.go:SA1019 \ + github.com/prometheus/client_golang/prometheus/http.go:SA1019 + +.PHONY: get_dep +get_dep: + @echo ">> getting dependencies" + $(GO) get -t ./... + +.PHONY: test +test: get_dep common-test + +.PHONY: test-short +test-short: get_dep common-test-short diff --git a/vendor/github.com/prometheus/client_golang/Makefile.common b/vendor/github.com/prometheus/client_golang/Makefile.common new file mode 100644 index 0000000000..741579e60f --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/Makefile.common @@ -0,0 +1,223 @@ +# Copyright 2018 The Prometheus Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# A common Makefile that includes rules to be reused in different prometheus projects. +# !!! Open PRs only against the prometheus/prometheus/Makefile.common repository! + +# Example usage : +# Create the main Makefile in the root project directory. +# include Makefile.common +# customTarget: +# @echo ">> Running customTarget" +# + +# Ensure GOBIN is not set during build so that promu is installed to the correct path +unexport GOBIN + +GO ?= go +GOFMT ?= $(GO)fmt +FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) +GOOPTS ?= + +GO_VERSION ?= $(shell $(GO) version) +GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) +PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') + +unexport GOVENDOR +ifeq (, $(PRE_GO_111)) + ifneq (,$(wildcard go.mod)) + # Enforce Go modules support just in case the directory is inside GOPATH (and for Travis CI). + GO111MODULE := on + + ifneq (,$(wildcard vendor)) + # Always use the local vendor/ directory to satisfy the dependencies. + GOOPTS := $(GOOPTS) -mod=vendor + endif + endif +else + ifneq (,$(wildcard go.mod)) + ifneq (,$(wildcard vendor)) +$(warning This repository requires Go >= 1.11 because of Go modules) +$(warning Some recipes may not work as expected as the current Go runtime is '$(GO_VERSION_NUMBER)') + endif + else + # This repository isn't using Go modules (yet). + GOVENDOR := $(FIRST_GOPATH)/bin/govendor + endif + + unexport GO111MODULE +endif +PROMU := $(FIRST_GOPATH)/bin/promu +STATICCHECK := $(FIRST_GOPATH)/bin/staticcheck +pkgs = ./... + +GO_VERSION ?= $(shell $(GO) version) +GO_BUILD_PLATFORM ?= $(subst /,-,$(lastword $(GO_VERSION))) + +PROMU_VERSION ?= 0.2.0 +PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz + +PREFIX ?= $(shell pwd) +BIN_DIR ?= $(shell pwd) +DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) +DOCKER_REPO ?= prom + +.PHONY: all +all: precheck style staticcheck unused build test + +# This rule is used to forward a target like "build" to "common-build". This +# allows a new "build" target to be defined in a Makefile which includes this +# one and override "common-build" without override warnings. +%: common-% ; + +.PHONY: common-style +common-style: + @echo ">> checking code style" + @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ + if [ -n "$${fmtRes}" ]; then \ + echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ + echo "Please ensure you are using $$($(GO) version) for formatting code."; \ + exit 1; \ + fi + +.PHONY: common-check_license +common-check_license: + @echo ">> checking license header" + @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ + awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ + done); \ + if [ -n "$${licRes}" ]; then \ + echo "license header checking failed:"; echo "$${licRes}"; \ + exit 1; \ + fi + +.PHONY: common-test-short +common-test-short: + @echo ">> running short tests" + GO111MODULE=$(GO111MODULE) $(GO) test -short $(GOOPTS) $(pkgs) + +.PHONY: common-test +common-test: + @echo ">> running all tests" + GO111MODULE=$(GO111MODULE) $(GO) test -race $(GOOPTS) $(pkgs) + +.PHONY: common-format +common-format: + @echo ">> formatting code" + GO111MODULE=$(GO111MODULE) $(GO) fmt $(GOOPTS) $(pkgs) + +.PHONY: common-vet +common-vet: + @echo ">> vetting code" + GO111MODULE=$(GO111MODULE) $(GO) vet $(GOOPTS) $(pkgs) + +.PHONY: common-staticcheck +common-staticcheck: $(STATICCHECK) + @echo ">> running staticcheck" +ifdef GO111MODULE + GO111MODULE=$(GO111MODULE) $(STATICCHECK) -ignore "$(STATICCHECK_IGNORE)" -checks "SA*" $(pkgs) +else + $(STATICCHECK) -ignore "$(STATICCHECK_IGNORE)" $(pkgs) +endif + +.PHONY: common-unused +common-unused: $(GOVENDOR) +ifdef GOVENDOR + @echo ">> running check for unused packages" + @$(GOVENDOR) list +unused | grep . && exit 1 || echo 'No unused packages' +else +ifdef GO111MODULE + @echo ">> running check for unused/missing packages in go.mod" + GO111MODULE=$(GO111MODULE) $(GO) mod tidy + @git diff --exit-code -- go.sum go.mod +ifneq (,$(wildcard vendor)) + @echo ">> running check for unused packages in vendor/" + GO111MODULE=$(GO111MODULE) $(GO) mod vendor + @git diff --exit-code -- go.sum go.mod vendor/ +endif +endif +endif + +.PHONY: common-build +common-build: promu + @echo ">> building binaries" + GO111MODULE=$(GO111MODULE) $(PROMU) build --prefix $(PREFIX) + +.PHONY: common-tarball +common-tarball: promu + @echo ">> building release tarball" + $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) + +.PHONY: common-docker +common-docker: + docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" . + +.PHONY: common-docker-publish +common-docker-publish: + docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)" + +.PHONY: common-docker-tag-latest +common-docker-tag-latest: + docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):latest" + +.PHONY: promu +promu: $(PROMU) + +$(PROMU): + curl -s -L $(PROMU_URL) | tar -xvz -C /tmp + mkdir -v -p $(FIRST_GOPATH)/bin + cp -v /tmp/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(PROMU) + +.PHONY: proto +proto: + @echo ">> generating code from proto files" + @./scripts/genproto.sh + +.PHONY: $(STATICCHECK) +$(STATICCHECK): +ifdef GO111MODULE +# Get staticcheck from a temporary directory to avoid modifying the local go.{mod,sum}. +# See https://github.com/golang/go/issues/27643. +# For now, we are using the next branch of staticcheck because master isn't compatible yet with Go modules. + tmpModule=$$(mktemp -d 2>&1) && \ + mkdir -p $${tmpModule}/staticcheck && \ + cd "$${tmpModule}"/staticcheck && \ + GO111MODULE=on $(GO) mod init example.com/staticcheck && \ + GO111MODULE=on GOOS= GOARCH= $(GO) get -u honnef.co/go/tools/cmd/staticcheck@next && \ + rm -rf $${tmpModule}; +else + GOOS= GOARCH= GO111MODULE=off $(GO) get -u honnef.co/go/tools/cmd/staticcheck +endif + +ifdef GOVENDOR +.PHONY: $(GOVENDOR) +$(GOVENDOR): + GOOS= GOARCH= $(GO) get -u github.com/kardianos/govendor +endif + +.PHONY: precheck +precheck:: + +define PRECHECK_COMMAND_template = +precheck:: $(1)_precheck + + +PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) +.PHONY: $(1)_precheck +$(1)_precheck: + @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ + echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ + exit 1; \ + fi +endef diff --git a/vendor/github.com/prometheus/client_golang/README.md b/vendor/github.com/prometheus/client_golang/README.md index 557eacf5a8..894a6a3e24 100644 --- a/vendor/github.com/prometheus/client_golang/README.md +++ b/vendor/github.com/prometheus/client_golang/README.md @@ -1,12 +1,64 @@ # Prometheus Go client library [![Build Status](https://travis-ci.org/prometheus/client_golang.svg?branch=master)](https://travis-ci.org/prometheus/client_golang) +[![Go Report Card](https://goreportcard.com/badge/github.com/prometheus/client_golang)](https://goreportcard.com/report/github.com/prometheus/client_golang) +[![go-doc](https://godoc.org/github.com/prometheus/client_golang?status.svg)](https://godoc.org/github.com/prometheus/client_golang) This is the [Go](http://golang.org) client library for [Prometheus](http://prometheus.io). It has two separate parts, one for instrumenting application code, and one for creating clients that talk to the Prometheus HTTP API. +__This library requires Go1.7 or later.__ + +## Important note about releases, versioning, tagging, and stability + +While our goal is to follow [Semantic Versioning](https://semver.org/), this +repository is still pre-1.0.0. To quote the +[Semantic Versioning spec](https://semver.org/#spec-item-4): “Anything may +change at any time. The public API should not be considered stable.” We know +that this is at odds with the widespread use of this library. However, just +declaring something 1.0.0 doesn't make it 1.0.0. Instead, we are working +towards a 1.0.0 release that actually deserves its major version number. + +Having said that, we aim for always keeping the tip of master in a workable +state. We occasionally tag versions and track their changes in CHANGELOG.md, +but this happens mostly to keep dependency management tools happy and to give +people a handle they can talk about easily. In particular, all commits in the +master branch have passed the same testing and reviewing. There is no QA +process in place that would render tagged commits more stable or better tested +than others. + +There is a plan behind the current (pre-1.0.0) versioning, though: + +- v0.9 is the “production release”, currently tracked in the master + branch. “Patch” releases will usually be just bug fixes, indeed, but + important new features that do not require invasive code changes might also + be included in those. We do not plan any breaking changes from one v0.9.x + release to any later v0.9.y release, but nothing is guaranteed. Since the + master branch will eventually be switched over to track the upcoming v0.10 + (see below), we recommend to tell your dependency management tool of choice + to use the latest v0.9.x release, at least for your production software. In + that way, you should get bug fixes and non-invasive, low-risk new features + without the need to change anything on your part. +- v0.10 is a planned release that will have a _lot_ of breaking changes + (despite being only a “minor” release in the Semantic Versioning terminology, + but as said, pre-1.0.0 means nothing is guaranteed). Essentially, we have + been piling up feature requests that require breaking changes for a while, + and they are all collected in the + [v0.10 milestone](https://github.com/prometheus/client_golang/milestone/2). + Since there will be so many breaking changes, the development for v0.10 is + currently not happening in the master branch, but in the + [dev-0.10 branch](https://github.com/prometheus/client_golang/tree/dev-0.10). + It will violently change for a while, and it will definitely be in a + non-working state now and then. It should only be used for sneak-peaks and + discussions of the new features and designs. +- Once v0.10 is ready for real-life use, it will be merged into the master + branch (which is the reason why you should lock your dependency management + tool to v0.9.x and only migrate to v0.10 when both you and v0.10 are ready + for it). In the ideal case, v0.10 will be the basis for the future v1.0 + release, but we cannot provide an ETA at this time. + ## Instrumenting applications [![code-coverage](http://gocover.io/_badge/github.com/prometheus/client_golang/prometheus)](http://gocover.io/github.com/prometheus/client_golang/prometheus) [![go-doc](https://godoc.org/github.com/prometheus/client_golang/prometheus?status.svg)](https://godoc.org/github.com/prometheus/client_golang/prometheus) @@ -14,8 +66,8 @@ Prometheus HTTP API. The [`prometheus` directory](https://github.com/prometheus/client_golang/tree/master/prometheus) contains the instrumentation library. See the -[best practices section](http://prometheus.io/docs/practices/naming/) of the -Prometheus documentation to learn more about instrumenting applications. +[guide](https://prometheus.io/docs/guides/go-application/) on the Prometheus +website to learn more about instrumenting applications. The [`examples` directory](https://github.com/prometheus/client_golang/tree/master/examples) @@ -23,13 +75,14 @@ contains simple examples of instrumented code. ## Client for the Prometheus HTTP API -[![code-coverage](http://gocover.io/_badge/github.com/prometheus/client_golang/api/prometheus)](http://gocover.io/github.com/prometheus/client_golang/api/prometheus) [![go-doc](https://godoc.org/github.com/prometheus/client_golang/api/prometheus?status.svg)](https://godoc.org/github.com/prometheus/client_golang/api/prometheus) +[![code-coverage](http://gocover.io/_badge/github.com/prometheus/client_golang/api/prometheus/v1)](http://gocover.io/github.com/prometheus/client_golang/api/prometheus/v1) [![go-doc](https://godoc.org/github.com/prometheus/client_golang/api/prometheus?status.svg)](https://godoc.org/github.com/prometheus/client_golang/api) The [`api/prometheus` directory](https://github.com/prometheus/client_golang/tree/master/api/prometheus) contains the client for the [Prometheus HTTP API](http://prometheus.io/docs/querying/api/). It allows you -to write Go applications that query time series data from a Prometheus server. +to write Go applications that query time series data from a Prometheus +server. It is still in alpha stage. ## Where is `model`, `extraction`, and `text`? diff --git a/vendor/github.com/prometheus/client_golang/VERSION b/vendor/github.com/prometheus/client_golang/VERSION index a3df0a6959..2003b639c4 100644 --- a/vendor/github.com/prometheus/client_golang/VERSION +++ b/vendor/github.com/prometheus/client_golang/VERSION @@ -1 +1 @@ -0.8.0 +0.9.2 diff --git a/vendor/github.com/prometheus/client_golang/api/client.go b/vendor/github.com/prometheus/client_golang/api/client.go new file mode 100644 index 0000000000..09af749caa --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/api/client.go @@ -0,0 +1,131 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build go1.7 + +// Package api provides clients for the HTTP APIs. +package api + +import ( + "context" + "io/ioutil" + "net" + "net/http" + "net/url" + "path" + "strings" + "time" +) + +// DefaultRoundTripper is used if no RoundTripper is set in Config. +var DefaultRoundTripper http.RoundTripper = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 10 * time.Second, +} + +// Config defines configuration parameters for a new client. +type Config struct { + // The address of the Prometheus to connect to. + Address string + + // RoundTripper is used by the Client to drive HTTP requests. If not + // provided, DefaultRoundTripper will be used. + RoundTripper http.RoundTripper +} + +func (cfg *Config) roundTripper() http.RoundTripper { + if cfg.RoundTripper == nil { + return DefaultRoundTripper + } + return cfg.RoundTripper +} + +// Client is the interface for an API client. +type Client interface { + URL(ep string, args map[string]string) *url.URL + Do(context.Context, *http.Request) (*http.Response, []byte, error) +} + +// NewClient returns a new Client. +// +// It is safe to use the returned Client from multiple goroutines. +func NewClient(cfg Config) (Client, error) { + u, err := url.Parse(cfg.Address) + if err != nil { + return nil, err + } + u.Path = strings.TrimRight(u.Path, "/") + + return &httpClient{ + endpoint: u, + client: http.Client{Transport: cfg.roundTripper()}, + }, nil +} + +type httpClient struct { + endpoint *url.URL + client http.Client +} + +func (c *httpClient) URL(ep string, args map[string]string) *url.URL { + p := path.Join(c.endpoint.Path, ep) + + for arg, val := range args { + arg = ":" + arg + p = strings.Replace(p, arg, val, -1) + } + + u := *c.endpoint + u.Path = p + + return &u +} + +func (c *httpClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { + if ctx != nil { + req = req.WithContext(ctx) + } + resp, err := c.client.Do(req) + defer func() { + if resp != nil { + resp.Body.Close() + } + }() + + if err != nil { + return nil, nil, err + } + + var body []byte + done := make(chan struct{}) + go func() { + body, err = ioutil.ReadAll(resp.Body) + close(done) + }() + + select { + case <-ctx.Done(): + err = resp.Body.Close() + <-done + if err == nil { + err = ctx.Err() + } + case <-done: + } + + return resp, body, err +} diff --git a/vendor/github.com/prometheus/client_golang/api/client_test.go b/vendor/github.com/prometheus/client_golang/api/client_test.go new file mode 100644 index 0000000000..b1bcfc9b8e --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/api/client_test.go @@ -0,0 +1,115 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build go1.7 + +package api + +import ( + "net/http" + "net/url" + "testing" +) + +func TestConfig(t *testing.T) { + c := Config{} + if c.roundTripper() != DefaultRoundTripper { + t.Fatalf("expected default roundtripper for nil RoundTripper field") + } +} + +func TestClientURL(t *testing.T) { + tests := []struct { + address string + endpoint string + args map[string]string + expected string + }{ + { + address: "http://localhost:9090", + endpoint: "/test", + expected: "http://localhost:9090/test", + }, + { + address: "http://localhost", + endpoint: "/test", + expected: "http://localhost/test", + }, + { + address: "http://localhost:9090", + endpoint: "test", + expected: "http://localhost:9090/test", + }, + { + address: "http://localhost:9090/prefix", + endpoint: "/test", + expected: "http://localhost:9090/prefix/test", + }, + { + address: "https://localhost:9090/", + endpoint: "/test/", + expected: "https://localhost:9090/test", + }, + { + address: "http://localhost:9090", + endpoint: "/test/:param", + args: map[string]string{ + "param": "content", + }, + expected: "http://localhost:9090/test/content", + }, + { + address: "http://localhost:9090", + endpoint: "/test/:param/more/:param", + args: map[string]string{ + "param": "content", + }, + expected: "http://localhost:9090/test/content/more/content", + }, + { + address: "http://localhost:9090", + endpoint: "/test/:param/more/:foo", + args: map[string]string{ + "param": "content", + "foo": "bar", + }, + expected: "http://localhost:9090/test/content/more/bar", + }, + { + address: "http://localhost:9090", + endpoint: "/test/:param", + args: map[string]string{ + "nonexistent": "content", + }, + expected: "http://localhost:9090/test/:param", + }, + } + + for _, test := range tests { + ep, err := url.Parse(test.address) + if err != nil { + t.Fatal(err) + } + + hclient := &httpClient{ + endpoint: ep, + client: http.Client{Transport: DefaultRoundTripper}, + } + + u := hclient.URL(test.endpoint, test.args) + if u.String() != test.expected { + t.Errorf("unexpected result: got %s, want %s", u, test.expected) + continue + } + } +} diff --git a/vendor/github.com/prometheus/client_golang/api/prometheus/api.go b/vendor/github.com/prometheus/client_golang/api/prometheus/api.go deleted file mode 100644 index 3028d741d5..0000000000 --- a/vendor/github.com/prometheus/client_golang/api/prometheus/api.go +++ /dev/null @@ -1,345 +0,0 @@ -// Copyright 2015 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package prometheus provides bindings to the Prometheus HTTP API: -// http://prometheus.io/docs/querying/api/ -package prometheus - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net" - "net/http" - "net/url" - "path" - "strconv" - "strings" - "time" - - "github.com/prometheus/common/model" - "golang.org/x/net/context" - "golang.org/x/net/context/ctxhttp" -) - -const ( - statusAPIError = 422 - apiPrefix = "/api/v1" - - epQuery = "/query" - epQueryRange = "/query_range" - epLabelValues = "/label/:name/values" - epSeries = "/series" -) - -type ErrorType string - -const ( - // The different API error types. - ErrBadData ErrorType = "bad_data" - ErrTimeout = "timeout" - ErrCanceled = "canceled" - ErrExec = "execution" - ErrBadResponse = "bad_response" -) - -// Error is an error returned by the API. -type Error struct { - Type ErrorType - Msg string -} - -func (e *Error) Error() string { - return fmt.Sprintf("%s: %s", e.Type, e.Msg) -} - -// CancelableTransport is like net.Transport but provides -// per-request cancelation functionality. -type CancelableTransport interface { - http.RoundTripper - CancelRequest(req *http.Request) -} - -var DefaultTransport CancelableTransport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).Dial, - TLSHandshakeTimeout: 10 * time.Second, -} - -// Config defines configuration parameters for a new client. -type Config struct { - // The address of the Prometheus to connect to. - Address string - - // Transport is used by the Client to drive HTTP requests. If not - // provided, DefaultTransport will be used. - Transport CancelableTransport -} - -func (cfg *Config) transport() CancelableTransport { - if cfg.Transport == nil { - return DefaultTransport - } - return cfg.Transport -} - -type Client interface { - url(ep string, args map[string]string) *url.URL - do(context.Context, *http.Request) (*http.Response, []byte, error) -} - -// New returns a new Client. -// -// It is safe to use the returned Client from multiple goroutines. -func New(cfg Config) (Client, error) { - u, err := url.Parse(cfg.Address) - if err != nil { - return nil, err - } - u.Path = strings.TrimRight(u.Path, "/") + apiPrefix - - return &httpClient{ - endpoint: u, - transport: cfg.transport(), - }, nil -} - -type httpClient struct { - endpoint *url.URL - transport CancelableTransport -} - -func (c *httpClient) url(ep string, args map[string]string) *url.URL { - p := path.Join(c.endpoint.Path, ep) - - for arg, val := range args { - arg = ":" + arg - p = strings.Replace(p, arg, val, -1) - } - - u := *c.endpoint - u.Path = p - - return &u -} - -func (c *httpClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { - resp, err := ctxhttp.Do(ctx, &http.Client{Transport: c.transport}, req) - - defer func() { - if resp != nil { - resp.Body.Close() - } - }() - - if err != nil { - return nil, nil, err - } - - var body []byte - done := make(chan struct{}) - go func() { - body, err = ioutil.ReadAll(resp.Body) - close(done) - }() - - select { - case <-ctx.Done(): - err = resp.Body.Close() - <-done - if err == nil { - err = ctx.Err() - } - case <-done: - } - - return resp, body, err -} - -// apiClient wraps a regular client and processes successful API responses. -// Successful also includes responses that errored at the API level. -type apiClient struct { - Client -} - -type apiResponse struct { - Status string `json:"status"` - Data json.RawMessage `json:"data"` - ErrorType ErrorType `json:"errorType"` - Error string `json:"error"` -} - -func (c apiClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { - resp, body, err := c.Client.do(ctx, req) - if err != nil { - return resp, body, err - } - - code := resp.StatusCode - - if code/100 != 2 && code != statusAPIError { - return resp, body, &Error{ - Type: ErrBadResponse, - Msg: fmt.Sprintf("bad response code %d", resp.StatusCode), - } - } - - var result apiResponse - - if err = json.Unmarshal(body, &result); err != nil { - return resp, body, &Error{ - Type: ErrBadResponse, - Msg: err.Error(), - } - } - - if (code == statusAPIError) != (result.Status == "error") { - err = &Error{ - Type: ErrBadResponse, - Msg: "inconsistent body for response code", - } - } - - if code == statusAPIError && result.Status == "error" { - err = &Error{ - Type: result.ErrorType, - Msg: result.Error, - } - } - - return resp, []byte(result.Data), err -} - -// Range represents a sliced time range. -type Range struct { - // The boundaries of the time range. - Start, End time.Time - // The maximum time between two slices within the boundaries. - Step time.Duration -} - -// queryResult contains result data for a query. -type queryResult struct { - Type model.ValueType `json:"resultType"` - Result interface{} `json:"result"` - - // The decoded value. - v model.Value -} - -func (qr *queryResult) UnmarshalJSON(b []byte) error { - v := struct { - Type model.ValueType `json:"resultType"` - Result json.RawMessage `json:"result"` - }{} - - err := json.Unmarshal(b, &v) - if err != nil { - return err - } - - switch v.Type { - case model.ValScalar: - var sv model.Scalar - err = json.Unmarshal(v.Result, &sv) - qr.v = &sv - - case model.ValVector: - var vv model.Vector - err = json.Unmarshal(v.Result, &vv) - qr.v = vv - - case model.ValMatrix: - var mv model.Matrix - err = json.Unmarshal(v.Result, &mv) - qr.v = mv - - default: - err = fmt.Errorf("unexpected value type %q", v.Type) - } - return err -} - -// QueryAPI provides bindings the Prometheus's query API. -type QueryAPI interface { - // Query performs a query for the given time. - Query(ctx context.Context, query string, ts time.Time) (model.Value, error) - // Query performs a query for the given range. - QueryRange(ctx context.Context, query string, r Range) (model.Value, error) -} - -// NewQueryAPI returns a new QueryAPI for the client. -// -// It is safe to use the returned QueryAPI from multiple goroutines. -func NewQueryAPI(c Client) QueryAPI { - return &httpQueryAPI{client: apiClient{c}} -} - -type httpQueryAPI struct { - client Client -} - -func (h *httpQueryAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, error) { - u := h.client.url(epQuery, nil) - q := u.Query() - - q.Set("query", query) - q.Set("time", ts.Format(time.RFC3339Nano)) - - u.RawQuery = q.Encode() - - req, _ := http.NewRequest("GET", u.String(), nil) - - _, body, err := h.client.do(ctx, req) - if err != nil { - return nil, err - } - - var qres queryResult - err = json.Unmarshal(body, &qres) - - return model.Value(qres.v), err -} - -func (h *httpQueryAPI) QueryRange(ctx context.Context, query string, r Range) (model.Value, error) { - u := h.client.url(epQueryRange, nil) - q := u.Query() - - var ( - start = r.Start.Format(time.RFC3339Nano) - end = r.End.Format(time.RFC3339Nano) - step = strconv.FormatFloat(r.Step.Seconds(), 'f', 3, 64) - ) - - q.Set("query", query) - q.Set("start", start) - q.Set("end", end) - q.Set("step", step) - - u.RawQuery = q.Encode() - - req, _ := http.NewRequest("GET", u.String(), nil) - - _, body, err := h.client.do(ctx, req) - if err != nil { - return nil, err - } - - var qres queryResult - err = json.Unmarshal(body, &qres) - - return model.Value(qres.v), err -} diff --git a/vendor/github.com/prometheus/client_golang/api/prometheus/api_test.go b/vendor/github.com/prometheus/client_golang/api/prometheus/api_test.go deleted file mode 100644 index 87d3e408e2..0000000000 --- a/vendor/github.com/prometheus/client_golang/api/prometheus/api_test.go +++ /dev/null @@ -1,453 +0,0 @@ -// Copyright 2015 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package prometheus - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "reflect" - "testing" - "time" - - "github.com/prometheus/common/model" - "golang.org/x/net/context" -) - -func TestConfig(t *testing.T) { - c := Config{} - if c.transport() != DefaultTransport { - t.Fatalf("expected default transport for nil Transport field") - } -} - -func TestClientURL(t *testing.T) { - tests := []struct { - address string - endpoint string - args map[string]string - expected string - }{ - { - address: "http://localhost:9090", - endpoint: "/test", - expected: "http://localhost:9090/test", - }, - { - address: "http://localhost", - endpoint: "/test", - expected: "http://localhost/test", - }, - { - address: "http://localhost:9090", - endpoint: "test", - expected: "http://localhost:9090/test", - }, - { - address: "http://localhost:9090/prefix", - endpoint: "/test", - expected: "http://localhost:9090/prefix/test", - }, - { - address: "https://localhost:9090/", - endpoint: "/test/", - expected: "https://localhost:9090/test", - }, - { - address: "http://localhost:9090", - endpoint: "/test/:param", - args: map[string]string{ - "param": "content", - }, - expected: "http://localhost:9090/test/content", - }, - { - address: "http://localhost:9090", - endpoint: "/test/:param/more/:param", - args: map[string]string{ - "param": "content", - }, - expected: "http://localhost:9090/test/content/more/content", - }, - { - address: "http://localhost:9090", - endpoint: "/test/:param/more/:foo", - args: map[string]string{ - "param": "content", - "foo": "bar", - }, - expected: "http://localhost:9090/test/content/more/bar", - }, - { - address: "http://localhost:9090", - endpoint: "/test/:param", - args: map[string]string{ - "nonexistant": "content", - }, - expected: "http://localhost:9090/test/:param", - }, - } - - for _, test := range tests { - ep, err := url.Parse(test.address) - if err != nil { - t.Fatal(err) - } - - hclient := &httpClient{ - endpoint: ep, - transport: DefaultTransport, - } - - u := hclient.url(test.endpoint, test.args) - if u.String() != test.expected { - t.Errorf("unexpected result: got %s, want %s", u, test.expected) - continue - } - - // The apiClient must return exactly the same result as the httpClient. - aclient := &apiClient{hclient} - - u = aclient.url(test.endpoint, test.args) - if u.String() != test.expected { - t.Errorf("unexpected result: got %s, want %s", u, test.expected) - } - } -} - -type testClient struct { - *testing.T - - ch chan apiClientTest - req *http.Request -} - -type apiClientTest struct { - code int - response interface{} - expected string - err *Error -} - -func (c *testClient) url(ep string, args map[string]string) *url.URL { - return nil -} - -func (c *testClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { - if ctx == nil { - c.Fatalf("context was not passed down") - } - if req != c.req { - c.Fatalf("request was not passed down") - } - - test := <-c.ch - - var b []byte - var err error - - switch v := test.response.(type) { - case string: - b = []byte(v) - default: - b, err = json.Marshal(v) - if err != nil { - c.Fatal(err) - } - } - - resp := &http.Response{ - StatusCode: test.code, - } - - return resp, b, nil -} - -func TestAPIClientDo(t *testing.T) { - tests := []apiClientTest{ - { - response: &apiResponse{ - Status: "error", - Data: json.RawMessage(`null`), - ErrorType: ErrBadData, - Error: "failed", - }, - err: &Error{ - Type: ErrBadData, - Msg: "failed", - }, - code: statusAPIError, - expected: `null`, - }, - { - response: &apiResponse{ - Status: "error", - Data: json.RawMessage(`"test"`), - ErrorType: ErrTimeout, - Error: "timed out", - }, - err: &Error{ - Type: ErrTimeout, - Msg: "timed out", - }, - code: statusAPIError, - expected: `test`, - }, - { - response: "bad json", - err: &Error{ - Type: ErrBadResponse, - Msg: "bad response code 400", - }, - code: http.StatusBadRequest, - }, - { - response: "bad json", - err: &Error{ - Type: ErrBadResponse, - Msg: "invalid character 'b' looking for beginning of value", - }, - code: statusAPIError, - }, - { - response: &apiResponse{ - Status: "success", - Data: json.RawMessage(`"test"`), - }, - err: &Error{ - Type: ErrBadResponse, - Msg: "inconsistent body for response code", - }, - code: statusAPIError, - }, - { - response: &apiResponse{ - Status: "success", - Data: json.RawMessage(`"test"`), - ErrorType: ErrTimeout, - Error: "timed out", - }, - err: &Error{ - Type: ErrBadResponse, - Msg: "inconsistent body for response code", - }, - code: statusAPIError, - }, - { - response: &apiResponse{ - Status: "error", - Data: json.RawMessage(`"test"`), - ErrorType: ErrTimeout, - Error: "timed out", - }, - err: &Error{ - Type: ErrBadResponse, - Msg: "inconsistent body for response code", - }, - code: http.StatusOK, - }, - } - - tc := &testClient{ - T: t, - ch: make(chan apiClientTest, 1), - req: &http.Request{}, - } - client := &apiClient{tc} - - for _, test := range tests { - - tc.ch <- test - - _, body, err := client.do(context.Background(), tc.req) - - if test.err != nil { - if err == nil { - t.Errorf("expected error %q but got none", test.err) - continue - } - if test.err.Error() != err.Error() { - t.Errorf("unexpected error: want %q, got %q", test.err, err) - } - continue - } - if err != nil { - t.Errorf("unexpeceted error %s", err) - continue - } - - want, got := test.expected, string(body) - if want != got { - t.Errorf("unexpected body: want %q, got %q", want, got) - } - } -} - -type apiTestClient struct { - *testing.T - curTest apiTest -} - -type apiTest struct { - do func() (interface{}, error) - inErr error - inRes interface{} - - reqPath string - reqParam url.Values - reqMethod string - res interface{} - err error -} - -func (c *apiTestClient) url(ep string, args map[string]string) *url.URL { - u := &url.URL{ - Host: "test:9090", - Path: apiPrefix + ep, - } - return u -} - -func (c *apiTestClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { - - test := c.curTest - - if req.URL.Path != test.reqPath { - c.Errorf("unexpected request path: want %s, got %s", test.reqPath, req.URL.Path) - } - if req.Method != test.reqMethod { - c.Errorf("unexpected request method: want %s, got %s", test.reqMethod, req.Method) - } - - b, err := json.Marshal(test.inRes) - if err != nil { - c.Fatal(err) - } - - resp := &http.Response{} - if test.inErr != nil { - resp.StatusCode = statusAPIError - } else { - resp.StatusCode = http.StatusOK - } - - return resp, b, test.inErr -} - -func TestAPIs(t *testing.T) { - - testTime := time.Now() - - client := &apiTestClient{T: t} - - queryApi := &httpQueryAPI{ - client: client, - } - - doQuery := func(q string, ts time.Time) func() (interface{}, error) { - return func() (interface{}, error) { - return queryApi.Query(context.Background(), q, ts) - } - } - - doQueryRange := func(q string, rng Range) func() (interface{}, error) { - return func() (interface{}, error) { - return queryApi.QueryRange(context.Background(), q, rng) - } - } - - queryTests := []apiTest{ - { - do: doQuery("2", testTime), - inRes: &queryResult{ - Type: model.ValScalar, - Result: &model.Scalar{ - Value: 2, - Timestamp: model.TimeFromUnix(testTime.Unix()), - }, - }, - - reqMethod: "GET", - reqPath: "/api/v1/query", - reqParam: url.Values{ - "query": []string{"2"}, - "time": []string{testTime.Format(time.RFC3339Nano)}, - }, - res: &model.Scalar{ - Value: 2, - Timestamp: model.TimeFromUnix(testTime.Unix()), - }, - }, - { - do: doQuery("2", testTime), - inErr: fmt.Errorf("some error"), - - reqMethod: "GET", - reqPath: "/api/v1/query", - reqParam: url.Values{ - "query": []string{"2"}, - "time": []string{testTime.Format(time.RFC3339Nano)}, - }, - err: fmt.Errorf("some error"), - }, - - { - do: doQueryRange("2", Range{ - Start: testTime.Add(-time.Minute), - End: testTime, - Step: time.Minute, - }), - inErr: fmt.Errorf("some error"), - - reqMethod: "GET", - reqPath: "/api/v1/query_range", - reqParam: url.Values{ - "query": []string{"2"}, - "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, - "end": []string{testTime.Format(time.RFC3339Nano)}, - "step": []string{time.Minute.String()}, - }, - err: fmt.Errorf("some error"), - }, - } - - var tests []apiTest - tests = append(tests, queryTests...) - - for _, test := range tests { - client.curTest = test - - res, err := test.do() - - if test.err != nil { - if err == nil { - t.Errorf("expected error %q but got none", test.err) - continue - } - if err.Error() != test.err.Error() { - t.Errorf("unexpected error: want %s, got %s", test.err, err) - } - continue - } - if err != nil { - t.Errorf("unexpected error: %s", err) - continue - } - - if !reflect.DeepEqual(res, test.res) { - t.Errorf("unexpected result: want %v, got %v", test.res, res) - } - } -} diff --git a/vendor/github.com/prometheus/client_golang/api/prometheus/v1/api.go b/vendor/github.com/prometheus/client_golang/api/prometheus/v1/api.go new file mode 100644 index 0000000000..642418890c --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/api/prometheus/v1/api.go @@ -0,0 +1,519 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build go1.7 + +// Package v1 provides bindings to the Prometheus HTTP API v1: +// http://prometheus.io/docs/querying/api/ +package v1 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/prometheus/client_golang/api" + "github.com/prometheus/common/model" +) + +const ( + statusAPIError = 422 + + apiPrefix = "/api/v1" + + epAlertManagers = apiPrefix + "/alertmanagers" + epQuery = apiPrefix + "/query" + epQueryRange = apiPrefix + "/query_range" + epLabelValues = apiPrefix + "/label/:name/values" + epSeries = apiPrefix + "/series" + epTargets = apiPrefix + "/targets" + epSnapshot = apiPrefix + "/admin/tsdb/snapshot" + epDeleteSeries = apiPrefix + "/admin/tsdb/delete_series" + epCleanTombstones = apiPrefix + "/admin/tsdb/clean_tombstones" + epConfig = apiPrefix + "/status/config" + epFlags = apiPrefix + "/status/flags" +) + +// ErrorType models the different API error types. +type ErrorType string + +// HealthStatus models the health status of a scrape target. +type HealthStatus string + +const ( + // Possible values for ErrorType. + ErrBadData ErrorType = "bad_data" + ErrTimeout ErrorType = "timeout" + ErrCanceled ErrorType = "canceled" + ErrExec ErrorType = "execution" + ErrBadResponse ErrorType = "bad_response" + ErrServer ErrorType = "server_error" + ErrClient ErrorType = "client_error" + + // Possible values for HealthStatus. + HealthGood HealthStatus = "up" + HealthUnknown HealthStatus = "unknown" + HealthBad HealthStatus = "down" +) + +// Error is an error returned by the API. +type Error struct { + Type ErrorType + Msg string + Detail string +} + +func (e *Error) Error() string { + return fmt.Sprintf("%s: %s", e.Type, e.Msg) +} + +// Range represents a sliced time range. +type Range struct { + // The boundaries of the time range. + Start, End time.Time + // The maximum time between two slices within the boundaries. + Step time.Duration +} + +// API provides bindings for Prometheus's v1 API. +type API interface { + // AlertManagers returns an overview of the current state of the Prometheus alert manager discovery. + AlertManagers(ctx context.Context) (AlertManagersResult, error) + // CleanTombstones removes the deleted data from disk and cleans up the existing tombstones. + CleanTombstones(ctx context.Context) error + // Config returns the current Prometheus configuration. + Config(ctx context.Context) (ConfigResult, error) + // DeleteSeries deletes data for a selection of series in a time range. + DeleteSeries(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) error + // Flags returns the flag values that Prometheus was launched with. + Flags(ctx context.Context) (FlagsResult, error) + // LabelValues performs a query for the values of the given label. + LabelValues(ctx context.Context, label string) (model.LabelValues, error) + // Query performs a query for the given time. + Query(ctx context.Context, query string, ts time.Time) (model.Value, error) + // QueryRange performs a query for the given range. + QueryRange(ctx context.Context, query string, r Range) (model.Value, error) + // Series finds series by label matchers. + Series(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]model.LabelSet, error) + // Snapshot creates a snapshot of all current data into snapshots/- + // under the TSDB's data directory and returns the directory as response. + Snapshot(ctx context.Context, skipHead bool) (SnapshotResult, error) + // Targets returns an overview of the current state of the Prometheus target discovery. + Targets(ctx context.Context) (TargetsResult, error) +} + +// AlertManagersResult contains the result from querying the alertmanagers endpoint. +type AlertManagersResult struct { + Active []AlertManager `json:"activeAlertManagers"` + Dropped []AlertManager `json:"droppedAlertManagers"` +} + +// AlertManager models a configured Alert Manager. +type AlertManager struct { + URL string `json:"url"` +} + +// ConfigResult contains the result from querying the config endpoint. +type ConfigResult struct { + YAML string `json:"yaml"` +} + +// FlagsResult contains the result from querying the flag endpoint. +type FlagsResult map[string]string + +// SnapshotResult contains the result from querying the snapshot endpoint. +type SnapshotResult struct { + Name string `json:"name"` +} + +// TargetsResult contains the result from querying the targets endpoint. +type TargetsResult struct { + Active []ActiveTarget `json:"activeTargets"` + Dropped []DroppedTarget `json:"droppedTargets"` +} + +// ActiveTarget models an active Prometheus scrape target. +type ActiveTarget struct { + DiscoveredLabels model.LabelSet `json:"discoveredLabels"` + Labels model.LabelSet `json:"labels"` + ScrapeURL string `json:"scrapeUrl"` + LastError string `json:"lastError"` + LastScrape time.Time `json:"lastScrape"` + Health HealthStatus `json:"health"` +} + +// DroppedTarget models a dropped Prometheus scrape target. +type DroppedTarget struct { + DiscoveredLabels model.LabelSet `json:"discoveredLabels"` +} + +// queryResult contains result data for a query. +type queryResult struct { + Type model.ValueType `json:"resultType"` + Result interface{} `json:"result"` + + // The decoded value. + v model.Value +} + +func (qr *queryResult) UnmarshalJSON(b []byte) error { + v := struct { + Type model.ValueType `json:"resultType"` + Result json.RawMessage `json:"result"` + }{} + + err := json.Unmarshal(b, &v) + if err != nil { + return err + } + + switch v.Type { + case model.ValScalar: + var sv model.Scalar + err = json.Unmarshal(v.Result, &sv) + qr.v = &sv + + case model.ValVector: + var vv model.Vector + err = json.Unmarshal(v.Result, &vv) + qr.v = vv + + case model.ValMatrix: + var mv model.Matrix + err = json.Unmarshal(v.Result, &mv) + qr.v = mv + + default: + err = fmt.Errorf("unexpected value type %q", v.Type) + } + return err +} + +// NewAPI returns a new API for the client. +// +// It is safe to use the returned API from multiple goroutines. +func NewAPI(c api.Client) API { + return &httpAPI{client: apiClient{c}} +} + +type httpAPI struct { + client api.Client +} + +func (h *httpAPI) AlertManagers(ctx context.Context) (AlertManagersResult, error) { + u := h.client.URL(epAlertManagers, nil) + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return AlertManagersResult{}, err + } + + _, body, err := h.client.Do(ctx, req) + if err != nil { + return AlertManagersResult{}, err + } + + var res AlertManagersResult + err = json.Unmarshal(body, &res) + return res, err +} + +func (h *httpAPI) CleanTombstones(ctx context.Context) error { + u := h.client.URL(epCleanTombstones, nil) + + req, err := http.NewRequest(http.MethodPost, u.String(), nil) + if err != nil { + return err + } + + _, _, err = h.client.Do(ctx, req) + return err +} + +func (h *httpAPI) Config(ctx context.Context) (ConfigResult, error) { + u := h.client.URL(epConfig, nil) + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return ConfigResult{}, err + } + + _, body, err := h.client.Do(ctx, req) + if err != nil { + return ConfigResult{}, err + } + + var res ConfigResult + err = json.Unmarshal(body, &res) + return res, err +} + +func (h *httpAPI) DeleteSeries(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) error { + u := h.client.URL(epDeleteSeries, nil) + q := u.Query() + + for _, m := range matches { + q.Add("match[]", m) + } + + q.Set("start", startTime.Format(time.RFC3339Nano)) + q.Set("end", endTime.Format(time.RFC3339Nano)) + + u.RawQuery = q.Encode() + + req, err := http.NewRequest(http.MethodPost, u.String(), nil) + if err != nil { + return err + } + + _, _, err = h.client.Do(ctx, req) + return err +} + +func (h *httpAPI) Flags(ctx context.Context) (FlagsResult, error) { + u := h.client.URL(epFlags, nil) + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return FlagsResult{}, err + } + + _, body, err := h.client.Do(ctx, req) + if err != nil { + return FlagsResult{}, err + } + + var res FlagsResult + err = json.Unmarshal(body, &res) + return res, err +} + +func (h *httpAPI) LabelValues(ctx context.Context, label string) (model.LabelValues, error) { + u := h.client.URL(epLabelValues, map[string]string{"name": label}) + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + _, body, err := h.client.Do(ctx, req) + if err != nil { + return nil, err + } + var labelValues model.LabelValues + err = json.Unmarshal(body, &labelValues) + return labelValues, err +} + +func (h *httpAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, error) { + u := h.client.URL(epQuery, nil) + q := u.Query() + + q.Set("query", query) + if !ts.IsZero() { + q.Set("time", ts.Format(time.RFC3339Nano)) + } + + u.RawQuery = q.Encode() + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + + _, body, err := h.client.Do(ctx, req) + if err != nil { + return nil, err + } + + var qres queryResult + err = json.Unmarshal(body, &qres) + + return model.Value(qres.v), err +} + +func (h *httpAPI) QueryRange(ctx context.Context, query string, r Range) (model.Value, error) { + u := h.client.URL(epQueryRange, nil) + q := u.Query() + + var ( + start = r.Start.Format(time.RFC3339Nano) + end = r.End.Format(time.RFC3339Nano) + step = strconv.FormatFloat(r.Step.Seconds(), 'f', 3, 64) + ) + + q.Set("query", query) + q.Set("start", start) + q.Set("end", end) + q.Set("step", step) + + u.RawQuery = q.Encode() + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + + _, body, err := h.client.Do(ctx, req) + if err != nil { + return nil, err + } + + var qres queryResult + err = json.Unmarshal(body, &qres) + + return model.Value(qres.v), err +} + +func (h *httpAPI) Series(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]model.LabelSet, error) { + u := h.client.URL(epSeries, nil) + q := u.Query() + + for _, m := range matches { + q.Add("match[]", m) + } + + q.Set("start", startTime.Format(time.RFC3339Nano)) + q.Set("end", endTime.Format(time.RFC3339Nano)) + + u.RawQuery = q.Encode() + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + + _, body, err := h.client.Do(ctx, req) + if err != nil { + return nil, err + } + + var mset []model.LabelSet + err = json.Unmarshal(body, &mset) + return mset, err +} + +func (h *httpAPI) Snapshot(ctx context.Context, skipHead bool) (SnapshotResult, error) { + u := h.client.URL(epSnapshot, nil) + q := u.Query() + + q.Set("skip_head", strconv.FormatBool(skipHead)) + + u.RawQuery = q.Encode() + + req, err := http.NewRequest(http.MethodPost, u.String(), nil) + if err != nil { + return SnapshotResult{}, err + } + + _, body, err := h.client.Do(ctx, req) + if err != nil { + return SnapshotResult{}, err + } + + var res SnapshotResult + err = json.Unmarshal(body, &res) + return res, err +} + +func (h *httpAPI) Targets(ctx context.Context) (TargetsResult, error) { + u := h.client.URL(epTargets, nil) + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return TargetsResult{}, err + } + + _, body, err := h.client.Do(ctx, req) + if err != nil { + return TargetsResult{}, err + } + + var res TargetsResult + err = json.Unmarshal(body, &res) + return res, err +} + +// apiClient wraps a regular client and processes successful API responses. +// Successful also includes responses that errored at the API level. +type apiClient struct { + api.Client +} + +type apiResponse struct { + Status string `json:"status"` + Data json.RawMessage `json:"data"` + ErrorType ErrorType `json:"errorType"` + Error string `json:"error"` +} + +func apiError(code int) bool { + // These are the codes that Prometheus sends when it returns an error. + return code == statusAPIError || code == http.StatusBadRequest +} + +func errorTypeAndMsgFor(resp *http.Response) (ErrorType, string) { + switch resp.StatusCode / 100 { + case 4: + return ErrClient, fmt.Sprintf("client error: %d", resp.StatusCode) + case 5: + return ErrServer, fmt.Sprintf("server error: %d", resp.StatusCode) + } + return ErrBadResponse, fmt.Sprintf("bad response code %d", resp.StatusCode) +} + +func (c apiClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { + resp, body, err := c.Client.Do(ctx, req) + if err != nil { + return resp, body, err + } + + code := resp.StatusCode + + if code/100 != 2 && !apiError(code) { + errorType, errorMsg := errorTypeAndMsgFor(resp) + return resp, body, &Error{ + Type: errorType, + Msg: errorMsg, + Detail: string(body), + } + } + + var result apiResponse + + if http.StatusNoContent != code { + if err = json.Unmarshal(body, &result); err != nil { + return resp, body, &Error{ + Type: ErrBadResponse, + Msg: err.Error(), + } + } + } + + if apiError(code) != (result.Status == "error") { + err = &Error{ + Type: ErrBadResponse, + Msg: "inconsistent body for response code", + } + } + + if apiError(code) && result.Status == "error" { + err = &Error{ + Type: result.ErrorType, + Msg: result.Error, + } + } + + return resp, []byte(result.Data), err +} diff --git a/vendor/github.com/prometheus/client_golang/api/prometheus/v1/api_test.go b/vendor/github.com/prometheus/client_golang/api/prometheus/v1/api_test.go new file mode 100644 index 0000000000..8492a5c38f --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/api/prometheus/v1/api_test.go @@ -0,0 +1,768 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build go1.7 + +package v1 + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "reflect" + "strings" + "testing" + "time" + + "github.com/prometheus/common/model" +) + +type apiTest struct { + do func() (interface{}, error) + inErr error + inStatusCode int + inRes interface{} + + reqPath string + reqParam url.Values + reqMethod string + res interface{} + err error +} + +type apiTestClient struct { + *testing.T + curTest apiTest +} + +func (c *apiTestClient) URL(ep string, args map[string]string) *url.URL { + path := ep + for k, v := range args { + path = strings.Replace(path, ":"+k, v, -1) + } + u := &url.URL{ + Host: "test:9090", + Path: path, + } + return u +} + +func (c *apiTestClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { + + test := c.curTest + + if req.URL.Path != test.reqPath { + c.Errorf("unexpected request path: want %s, got %s", test.reqPath, req.URL.Path) + } + if req.Method != test.reqMethod { + c.Errorf("unexpected request method: want %s, got %s", test.reqMethod, req.Method) + } + + b, err := json.Marshal(test.inRes) + if err != nil { + c.Fatal(err) + } + + resp := &http.Response{} + if test.inStatusCode != 0 { + resp.StatusCode = test.inStatusCode + } else if test.inErr != nil { + resp.StatusCode = statusAPIError + } else { + resp.StatusCode = http.StatusOK + } + + return resp, b, test.inErr +} + +func TestAPIs(t *testing.T) { + + testTime := time.Now() + + client := &apiTestClient{T: t} + + promAPI := &httpAPI{ + client: client, + } + + doAlertManagers := func() func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.AlertManagers(context.Background()) + } + } + + doCleanTombstones := func() func() (interface{}, error) { + return func() (interface{}, error) { + return nil, promAPI.CleanTombstones(context.Background()) + } + } + + doConfig := func() func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.Config(context.Background()) + } + } + + doDeleteSeries := func(matcher string, startTime time.Time, endTime time.Time) func() (interface{}, error) { + return func() (interface{}, error) { + return nil, promAPI.DeleteSeries(context.Background(), []string{matcher}, startTime, endTime) + } + } + + doFlags := func() func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.Flags(context.Background()) + } + } + + doLabelValues := func(label string) func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.LabelValues(context.Background(), label) + } + } + + doQuery := func(q string, ts time.Time) func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.Query(context.Background(), q, ts) + } + } + + doQueryRange := func(q string, rng Range) func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.QueryRange(context.Background(), q, rng) + } + } + + doSeries := func(matcher string, startTime time.Time, endTime time.Time) func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.Series(context.Background(), []string{matcher}, startTime, endTime) + } + } + + doSnapshot := func(skipHead bool) func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.Snapshot(context.Background(), skipHead) + } + } + + doTargets := func() func() (interface{}, error) { + return func() (interface{}, error) { + return promAPI.Targets(context.Background()) + } + } + + queryTests := []apiTest{ + { + do: doQuery("2", testTime), + inRes: &queryResult{ + Type: model.ValScalar, + Result: &model.Scalar{ + Value: 2, + Timestamp: model.TimeFromUnix(testTime.Unix()), + }, + }, + + reqMethod: "GET", + reqPath: "/api/v1/query", + reqParam: url.Values{ + "query": []string{"2"}, + "time": []string{testTime.Format(time.RFC3339Nano)}, + }, + res: &model.Scalar{ + Value: 2, + Timestamp: model.TimeFromUnix(testTime.Unix()), + }, + }, + { + do: doQuery("2", testTime), + inErr: fmt.Errorf("some error"), + + reqMethod: "GET", + reqPath: "/api/v1/query", + reqParam: url.Values{ + "query": []string{"2"}, + "time": []string{testTime.Format(time.RFC3339Nano)}, + }, + err: fmt.Errorf("some error"), + }, + { + do: doQuery("2", testTime), + inRes: "some body", + inStatusCode: 500, + inErr: &Error{ + Type: ErrServer, + Msg: "server error: 500", + Detail: "some body", + }, + + reqMethod: "GET", + reqPath: "/api/v1/query", + reqParam: url.Values{ + "query": []string{"2"}, + "time": []string{testTime.Format(time.RFC3339Nano)}, + }, + err: errors.New("server_error: server error: 500"), + }, + { + do: doQuery("2", testTime), + inRes: "some body", + inStatusCode: 404, + inErr: &Error{ + Type: ErrClient, + Msg: "client error: 404", + Detail: "some body", + }, + + reqMethod: "GET", + reqPath: "/api/v1/query", + reqParam: url.Values{ + "query": []string{"2"}, + "time": []string{testTime.Format(time.RFC3339Nano)}, + }, + err: errors.New("client_error: client error: 404"), + }, + + { + do: doQueryRange("2", Range{ + Start: testTime.Add(-time.Minute), + End: testTime, + Step: time.Minute, + }), + inErr: fmt.Errorf("some error"), + + reqMethod: "GET", + reqPath: "/api/v1/query_range", + reqParam: url.Values{ + "query": []string{"2"}, + "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, + "end": []string{testTime.Format(time.RFC3339Nano)}, + "step": []string{time.Minute.String()}, + }, + err: fmt.Errorf("some error"), + }, + + { + do: doLabelValues("mylabel"), + inRes: []string{"val1", "val2"}, + reqMethod: "GET", + reqPath: "/api/v1/label/mylabel/values", + res: model.LabelValues{"val1", "val2"}, + }, + + { + do: doLabelValues("mylabel"), + inErr: fmt.Errorf("some error"), + reqMethod: "GET", + reqPath: "/api/v1/label/mylabel/values", + err: fmt.Errorf("some error"), + }, + + { + do: doSeries("up", testTime.Add(-time.Minute), testTime), + inRes: []map[string]string{ + { + "__name__": "up", + "job": "prometheus", + "instance": "localhost:9090"}, + }, + reqMethod: "GET", + reqPath: "/api/v1/series", + reqParam: url.Values{ + "match": []string{"up"}, + "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, + "end": []string{testTime.Format(time.RFC3339Nano)}, + }, + res: []model.LabelSet{ + model.LabelSet{ + "__name__": "up", + "job": "prometheus", + "instance": "localhost:9090", + }, + }, + }, + + { + do: doSeries("up", testTime.Add(-time.Minute), testTime), + inErr: fmt.Errorf("some error"), + reqMethod: "GET", + reqPath: "/api/v1/series", + reqParam: url.Values{ + "match": []string{"up"}, + "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, + "end": []string{testTime.Format(time.RFC3339Nano)}, + }, + err: fmt.Errorf("some error"), + }, + + { + do: doSnapshot(true), + inRes: map[string]string{ + "name": "20171210T211224Z-2be650b6d019eb54", + }, + reqMethod: "POST", + reqPath: "/api/v1/admin/tsdb/snapshot", + reqParam: url.Values{ + "skip_head": []string{"true"}, + }, + res: SnapshotResult{ + Name: "20171210T211224Z-2be650b6d019eb54", + }, + }, + + { + do: doSnapshot(true), + inErr: fmt.Errorf("some error"), + reqMethod: "POST", + reqPath: "/api/v1/admin/tsdb/snapshot", + err: fmt.Errorf("some error"), + }, + + { + do: doCleanTombstones(), + reqMethod: "POST", + reqPath: "/api/v1/admin/tsdb/clean_tombstones", + }, + + { + do: doCleanTombstones(), + inErr: fmt.Errorf("some error"), + reqMethod: "POST", + reqPath: "/api/v1/admin/tsdb/clean_tombstones", + err: fmt.Errorf("some error"), + }, + + { + do: doDeleteSeries("up", testTime.Add(-time.Minute), testTime), + inRes: []map[string]string{ + { + "__name__": "up", + "job": "prometheus", + "instance": "localhost:9090"}, + }, + reqMethod: "POST", + reqPath: "/api/v1/admin/tsdb/delete_series", + reqParam: url.Values{ + "match": []string{"up"}, + "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, + "end": []string{testTime.Format(time.RFC3339Nano)}, + }, + }, + + { + do: doDeleteSeries("up", testTime.Add(-time.Minute), testTime), + inErr: fmt.Errorf("some error"), + reqMethod: "POST", + reqPath: "/api/v1/admin/tsdb/delete_series", + reqParam: url.Values{ + "match": []string{"up"}, + "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, + "end": []string{testTime.Format(time.RFC3339Nano)}, + }, + err: fmt.Errorf("some error"), + }, + + { + do: doConfig(), + reqMethod: "GET", + reqPath: "/api/v1/status/config", + inRes: map[string]string{ + "yaml": "", + }, + res: ConfigResult{ + YAML: "", + }, + }, + + { + do: doConfig(), + reqMethod: "GET", + reqPath: "/api/v1/status/config", + inErr: fmt.Errorf("some error"), + err: fmt.Errorf("some error"), + }, + + { + do: doFlags(), + reqMethod: "GET", + reqPath: "/api/v1/status/flags", + inRes: map[string]string{ + "alertmanager.notification-queue-capacity": "10000", + "alertmanager.timeout": "10s", + "log.level": "info", + "query.lookback-delta": "5m", + "query.max-concurrency": "20", + }, + res: FlagsResult{ + "alertmanager.notification-queue-capacity": "10000", + "alertmanager.timeout": "10s", + "log.level": "info", + "query.lookback-delta": "5m", + "query.max-concurrency": "20", + }, + }, + + { + do: doFlags(), + reqMethod: "GET", + reqPath: "/api/v1/status/flags", + inErr: fmt.Errorf("some error"), + err: fmt.Errorf("some error"), + }, + + { + do: doAlertManagers(), + reqMethod: "GET", + reqPath: "/api/v1/alertmanagers", + inRes: map[string]interface{}{ + "activeAlertManagers": []map[string]string{ + { + "url": "http://127.0.0.1:9091/api/v1/alerts", + }, + }, + "droppedAlertManagers": []map[string]string{ + { + "url": "http://127.0.0.1:9092/api/v1/alerts", + }, + }, + }, + res: AlertManagersResult{ + Active: []AlertManager{ + { + URL: "http://127.0.0.1:9091/api/v1/alerts", + }, + }, + Dropped: []AlertManager{ + { + URL: "http://127.0.0.1:9092/api/v1/alerts", + }, + }, + }, + }, + + { + do: doAlertManagers(), + reqMethod: "GET", + reqPath: "/api/v1/alertmanagers", + inErr: fmt.Errorf("some error"), + err: fmt.Errorf("some error"), + }, + + { + do: doTargets(), + reqMethod: "GET", + reqPath: "/api/v1/targets", + inRes: map[string]interface{}{ + "activeTargets": []map[string]interface{}{ + { + "discoveredLabels": map[string]string{ + "__address__": "127.0.0.1:9090", + "__metrics_path__": "/metrics", + "__scheme__": "http", + "job": "prometheus", + }, + "labels": map[string]string{ + "instance": "127.0.0.1:9090", + "job": "prometheus", + }, + "scrapeUrl": "http://127.0.0.1:9090", + "lastError": "error while scraping target", + "lastScrape": testTime.UTC().Format(time.RFC3339Nano), + "health": "up", + }, + }, + "droppedTargets": []map[string]interface{}{ + { + "discoveredLabels": map[string]string{ + "__address__": "127.0.0.1:9100", + "__metrics_path__": "/metrics", + "__scheme__": "http", + "job": "node", + }, + }, + }, + }, + res: TargetsResult{ + Active: []ActiveTarget{ + { + DiscoveredLabels: model.LabelSet{ + "__address__": "127.0.0.1:9090", + "__metrics_path__": "/metrics", + "__scheme__": "http", + "job": "prometheus", + }, + Labels: model.LabelSet{ + "instance": "127.0.0.1:9090", + "job": "prometheus", + }, + ScrapeURL: "http://127.0.0.1:9090", + LastError: "error while scraping target", + LastScrape: testTime.UTC(), + Health: HealthGood, + }, + }, + Dropped: []DroppedTarget{ + { + DiscoveredLabels: model.LabelSet{ + "__address__": "127.0.0.1:9100", + "__metrics_path__": "/metrics", + "__scheme__": "http", + "job": "node", + }, + }, + }, + }, + }, + + { + do: doTargets(), + reqMethod: "GET", + reqPath: "/api/v1/targets", + inErr: fmt.Errorf("some error"), + err: fmt.Errorf("some error"), + }, + } + + var tests []apiTest + tests = append(tests, queryTests...) + + for i, test := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + client.curTest = test + + res, err := test.do() + + if test.err != nil { + if err == nil { + t.Fatalf("expected error %q but got none", test.err) + } + if err.Error() != test.err.Error() { + t.Errorf("unexpected error: want %s, got %s", test.err, err) + } + if apiErr, ok := err.(*Error); ok { + if apiErr.Detail != test.inRes { + t.Errorf("%q should be %q", apiErr.Detail, test.inRes) + } + } + return + } + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !reflect.DeepEqual(res, test.res) { + t.Errorf("unexpected result: want %v, got %v", test.res, res) + } + }) + } +} + +type testClient struct { + *testing.T + + ch chan apiClientTest + req *http.Request +} + +type apiClientTest struct { + code int + response interface{} + expectedBody string + expectedErr *Error +} + +func (c *testClient) URL(ep string, args map[string]string) *url.URL { + return nil +} + +func (c *testClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { + if ctx == nil { + c.Fatalf("context was not passed down") + } + if req != c.req { + c.Fatalf("request was not passed down") + } + + test := <-c.ch + + var b []byte + var err error + + switch v := test.response.(type) { + case string: + b = []byte(v) + default: + b, err = json.Marshal(v) + if err != nil { + c.Fatal(err) + } + } + + resp := &http.Response{ + StatusCode: test.code, + } + + return resp, b, nil +} + +func TestAPIClientDo(t *testing.T) { + tests := []apiClientTest{ + { + code: statusAPIError, + response: &apiResponse{ + Status: "error", + Data: json.RawMessage(`null`), + ErrorType: ErrBadData, + Error: "failed", + }, + expectedErr: &Error{ + Type: ErrBadData, + Msg: "failed", + }, + expectedBody: `null`, + }, + { + code: statusAPIError, + response: &apiResponse{ + Status: "error", + Data: json.RawMessage(`"test"`), + ErrorType: ErrTimeout, + Error: "timed out", + }, + expectedErr: &Error{ + Type: ErrTimeout, + Msg: "timed out", + }, + expectedBody: `test`, + }, + { + code: http.StatusInternalServerError, + response: "500 error details", + expectedErr: &Error{ + Type: ErrServer, + Msg: "server error: 500", + Detail: "500 error details", + }, + }, + { + code: http.StatusNotFound, + response: "404 error details", + expectedErr: &Error{ + Type: ErrClient, + Msg: "client error: 404", + Detail: "404 error details", + }, + }, + { + code: http.StatusBadRequest, + response: &apiResponse{ + Status: "error", + Data: json.RawMessage(`null`), + ErrorType: ErrBadData, + Error: "end timestamp must not be before start time", + }, + expectedErr: &Error{ + Type: ErrBadData, + Msg: "end timestamp must not be before start time", + }, + }, + { + code: statusAPIError, + response: "bad json", + expectedErr: &Error{ + Type: ErrBadResponse, + Msg: "invalid character 'b' looking for beginning of value", + }, + }, + { + code: statusAPIError, + response: &apiResponse{ + Status: "success", + Data: json.RawMessage(`"test"`), + }, + expectedErr: &Error{ + Type: ErrBadResponse, + Msg: "inconsistent body for response code", + }, + }, + { + code: statusAPIError, + response: &apiResponse{ + Status: "success", + Data: json.RawMessage(`"test"`), + ErrorType: ErrTimeout, + Error: "timed out", + }, + expectedErr: &Error{ + Type: ErrBadResponse, + Msg: "inconsistent body for response code", + }, + }, + { + code: http.StatusOK, + response: &apiResponse{ + Status: "error", + Data: json.RawMessage(`"test"`), + ErrorType: ErrTimeout, + Error: "timed out", + }, + expectedErr: &Error{ + Type: ErrBadResponse, + Msg: "inconsistent body for response code", + }, + }, + } + + tc := &testClient{ + T: t, + ch: make(chan apiClientTest, 1), + req: &http.Request{}, + } + client := &apiClient{tc} + + for i, test := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + + tc.ch <- test + + _, body, err := client.Do(context.Background(), tc.req) + + if test.expectedErr != nil { + if err == nil { + t.Fatalf("expected error %q but got none", test.expectedErr) + } + if test.expectedErr.Error() != err.Error() { + t.Errorf("unexpected error: want %q, got %q", test.expectedErr, err) + } + if test.expectedErr.Detail != "" { + apiErr := err.(*Error) + if apiErr.Detail != test.expectedErr.Detail { + t.Errorf("unexpected error details: want %q, got %q", test.expectedErr.Detail, apiErr.Detail) + } + } + return + } + if err != nil { + t.Fatalf("unexpeceted error %s", err) + } + + want, got := test.expectedBody, string(body) + if want != got { + t.Errorf("unexpected body: want %q, got %q", want, got) + } + }) + + } +} diff --git a/vendor/github.com/prometheus/client_golang/examples/random/main.go b/vendor/github.com/prometheus/client_golang/examples/random/main.go index 5639571935..eef50d200d 100644 --- a/vendor/github.com/prometheus/client_golang/examples/random/main.go +++ b/vendor/github.com/prometheus/client_golang/examples/random/main.go @@ -18,19 +18,21 @@ package main import ( "flag" + "log" "math" "math/rand" "net/http" "time" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( addr = flag.String("listen-address", ":8080", "The address to listen on for HTTP requests.") - uniformDomain = flag.Float64("uniform.domain", 200, "The domain for the uniform distribution.") - normDomain = flag.Float64("normal.domain", 200, "The domain for the normal distribution.") - normMean = flag.Float64("normal.mean", 10, "The mean for the normal distribution.") + uniformDomain = flag.Float64("uniform.domain", 0.0002, "The domain for the uniform distribution.") + normDomain = flag.Float64("normal.domain", 0.0002, "The domain for the normal distribution.") + normMean = flag.Float64("normal.mean", 0.00001, "The mean for the normal distribution.") oscillationPeriod = flag.Duration("oscillation-period", 10*time.Minute, "The duration of the rate oscillation period.") ) @@ -40,8 +42,9 @@ var ( // differentiated via a "service" label. rpcDurations = prometheus.NewSummaryVec( prometheus.SummaryOpts{ - Name: "rpc_durations_microseconds", - Help: "RPC latency distributions.", + Name: "rpc_durations_seconds", + Help: "RPC latency distributions.", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }, []string{"service"}, ) @@ -50,7 +53,7 @@ var ( // normal distribution, with 20 buckets centered on the mean, each // half-sigma wide. rpcDurationsHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{ - Name: "rpc_durations_histogram_microseconds", + Name: "rpc_durations_histogram_seconds", Help: "RPC latency distributions.", Buckets: prometheus.LinearBuckets(*normMean-5**normDomain, .5**normDomain, 20), }) @@ -91,13 +94,13 @@ func main() { go func() { for { - v := rand.ExpFloat64() + v := rand.ExpFloat64() / 1e6 rpcDurations.WithLabelValues("exponential").Observe(v) time.Sleep(time.Duration(50*oscillationFactor()) * time.Millisecond) } }() // Expose the registered metrics via HTTP. - http.Handle("/metrics", prometheus.Handler()) - http.ListenAndServe(*addr, nil) + http.Handle("/metrics", promhttp.Handler()) + log.Fatal(http.ListenAndServe(*addr, nil)) } diff --git a/vendor/github.com/prometheus/client_golang/examples/simple/main.go b/vendor/github.com/prometheus/client_golang/examples/simple/main.go index 19620d2b3e..1fc23249a2 100644 --- a/vendor/github.com/prometheus/client_golang/examples/simple/main.go +++ b/vendor/github.com/prometheus/client_golang/examples/simple/main.go @@ -16,15 +16,16 @@ package main import ( "flag" + "log" "net/http" - "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" ) var addr = flag.String("listen-address", ":8080", "The address to listen on for HTTP requests.") func main() { flag.Parse() - http.Handle("/metrics", prometheus.Handler()) - http.ListenAndServe(*addr, nil) + http.Handle("/metrics", promhttp.Handler()) + log.Fatal(http.ListenAndServe(*addr, nil)) } diff --git a/vendor/github.com/prometheus/client_golang/go.mod b/vendor/github.com/prometheus/client_golang/go.mod new file mode 100644 index 0000000000..6ec1588cb5 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/go.mod @@ -0,0 +1,12 @@ +module github.com/prometheus/client_golang + +require ( + github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 + github.com/golang/protobuf v1.2.0 + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 + github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 + github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a + golang.org/x/net v0.0.0-20181201002055-351d144fa1fc + golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect +) diff --git a/vendor/github.com/prometheus/client_golang/go.sum b/vendor/github.com/prometheus/client_golang/go.sum new file mode 100644 index 0000000000..181c46d338 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/go.sum @@ -0,0 +1,16 @@ +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/vendor/github.com/prometheus/client_golang/prometheus/benchmark_test.go b/vendor/github.com/prometheus/client_golang/prometheus/benchmark_test.go index a3d86698bf..4a05721dcc 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/benchmark_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/benchmark_test.go @@ -129,8 +129,9 @@ func BenchmarkGaugeNoLabels(b *testing.B) { func BenchmarkSummaryWithLabelValues(b *testing.B) { m := NewSummaryVec( SummaryOpts{ - Name: "benchmark_summary", - Help: "A summary to benchmark it.", + Name: "benchmark_summary", + Help: "A summary to benchmark it.", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }, []string{"one", "two", "three"}, ) @@ -143,8 +144,9 @@ func BenchmarkSummaryWithLabelValues(b *testing.B) { func BenchmarkSummaryNoLabels(b *testing.B) { m := NewSummary(SummaryOpts{ - Name: "benchmark_summary", - Help: "A summary to benchmark it.", + Name: "benchmark_summary", + Help: "A summary to benchmark it.", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }, ) b.ReportAllocs() @@ -181,3 +183,17 @@ func BenchmarkHistogramNoLabels(b *testing.B) { m.Observe(3.1415) } } + +func BenchmarkParallelCounter(b *testing.B) { + c := NewCounter(CounterOpts{ + Name: "benchmark_counter", + Help: "A Counter to benchmark it.", + }) + b.ReportAllocs() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + c.Inc() + } + }) +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/collector.go b/vendor/github.com/prometheus/client_golang/prometheus/collector.go index 623d3d83fe..c0d70b2faf 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/collector.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/collector.go @@ -29,27 +29,72 @@ type Collector interface { // collected by this Collector to the provided channel and returns once // the last descriptor has been sent. The sent descriptors fulfill the // consistency and uniqueness requirements described in the Desc - // documentation. (It is valid if one and the same Collector sends - // duplicate descriptors. Those duplicates are simply ignored. However, - // two different Collectors must not send duplicate descriptors.) This - // method idempotently sends the same descriptors throughout the - // lifetime of the Collector. If a Collector encounters an error while - // executing this method, it must send an invalid descriptor (created - // with NewInvalidDesc) to signal the error to the registry. + // documentation. + // + // It is valid if one and the same Collector sends duplicate + // descriptors. Those duplicates are simply ignored. However, two + // different Collectors must not send duplicate descriptors. + // + // Sending no descriptor at all marks the Collector as “unchecked”, + // i.e. no checks will be performed at registration time, and the + // Collector may yield any Metric it sees fit in its Collect method. + // + // This method idempotently sends the same descriptors throughout the + // lifetime of the Collector. It may be called concurrently and + // therefore must be implemented in a concurrency safe way. + // + // If a Collector encounters an error while executing this method, it + // must send an invalid descriptor (created with NewInvalidDesc) to + // signal the error to the registry. Describe(chan<- *Desc) // Collect is called by the Prometheus registry when collecting // metrics. The implementation sends each collected metric via the // provided channel and returns once the last metric has been sent. The - // descriptor of each sent metric is one of those returned by - // Describe. Returned metrics that share the same descriptor must differ - // in their variable label values. This method may be called - // concurrently and must therefore be implemented in a concurrency safe - // way. Blocking occurs at the expense of total performance of rendering - // all registered metrics. Ideally, Collector implementations support - // concurrent readers. + // descriptor of each sent metric is one of those returned by Describe + // (unless the Collector is unchecked, see above). Returned metrics that + // share the same descriptor must differ in their variable label + // values. + // + // This method may be called concurrently and must therefore be + // implemented in a concurrency safe way. Blocking occurs at the expense + // of total performance of rendering all registered metrics. Ideally, + // Collector implementations support concurrent readers. Collect(chan<- Metric) } +// DescribeByCollect is a helper to implement the Describe method of a custom +// Collector. It collects the metrics from the provided Collector and sends +// their descriptors to the provided channel. +// +// If a Collector collects the same metrics throughout its lifetime, its +// Describe method can simply be implemented as: +// +// func (c customCollector) Describe(ch chan<- *Desc) { +// DescribeByCollect(c, ch) +// } +// +// However, this will not work if the metrics collected change dynamically over +// the lifetime of the Collector in a way that their combined set of descriptors +// changes as well. The shortcut implementation will then violate the contract +// of the Describe method. If a Collector sometimes collects no metrics at all +// (for example vectors like CounterVec, GaugeVec, etc., which only collect +// metrics after a metric with a fully specified label set has been accessed), +// it might even get registered as an unchecked Collecter (cf. the Register +// method of the Registerer interface). Hence, only use this shortcut +// implementation of Describe if you are certain to fulfill the contract. +// +// The Collector example demonstrates a use of DescribeByCollect. +func DescribeByCollect(c Collector, descs chan<- *Desc) { + metrics := make(chan Metric) + go func() { + c.Collect(metrics) + close(metrics) + }() + for m := range metrics { + descs <- m.Desc() + } +} + // selfCollector implements Collector for a single Metric so that the Metric // collects itself. Add it as an anonymous field to a struct that implements // Metric, and call init with the Metric itself as an argument. diff --git a/vendor/github.com/prometheus/client_golang/prometheus/collector_test.go b/vendor/github.com/prometheus/client_golang/prometheus/collector_test.go new file mode 100644 index 0000000000..45eab3ea4a --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/collector_test.go @@ -0,0 +1,62 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import "testing" + +type collectorDescribedByCollect struct { + cnt Counter + gge Gauge +} + +func (c collectorDescribedByCollect) Collect(ch chan<- Metric) { + ch <- c.cnt + ch <- c.gge +} + +func (c collectorDescribedByCollect) Describe(ch chan<- *Desc) { + DescribeByCollect(c, ch) +} + +func TestDescribeByCollect(t *testing.T) { + + goodCollector := collectorDescribedByCollect{ + cnt: NewCounter(CounterOpts{Name: "c1", Help: "help c1"}), + gge: NewGauge(GaugeOpts{Name: "g1", Help: "help g1"}), + } + collidingCollector := collectorDescribedByCollect{ + cnt: NewCounter(CounterOpts{Name: "c2", Help: "help c2"}), + gge: NewGauge(GaugeOpts{Name: "g1", Help: "help g1"}), + } + inconsistentCollector := collectorDescribedByCollect{ + cnt: NewCounter(CounterOpts{Name: "c3", Help: "help c3"}), + gge: NewGauge(GaugeOpts{Name: "c3", Help: "help inconsistent"}), + } + + reg := NewPedanticRegistry() + + if err := reg.Register(goodCollector); err != nil { + t.Error("registration failed:", err) + } + if err := reg.Register(collidingCollector); err == nil { + t.Error("registration unexpectedly succeeded") + } + if err := reg.Register(inconsistentCollector); err == nil { + t.Error("registration unexpectedly succeeded") + } + + if _, err := reg.Gather(); err != nil { + t.Error("gathering failed:", err) + } +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/counter.go b/vendor/github.com/prometheus/client_golang/prometheus/counter.go index ee37949ada..d463e36d3e 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/counter.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/counter.go @@ -15,6 +15,10 @@ package prometheus import ( "errors" + "math" + "sync/atomic" + + dto "github.com/prometheus/client_model/go" ) // Counter is a Metric that represents a single numerical value that only ever @@ -30,16 +34,8 @@ type Counter interface { Metric Collector - // Set is used to set the Counter to an arbitrary value. It is only used - // if you have to transfer a value from an external counter into this - // Prometheus metric. Do not use it for regular handling of a - // Prometheus counter (as it can be used to break the contract of - // monotonically increasing values). - // - // Deprecated: Use NewConstMetric to create a counter for an external - // value. A Counter should never be set. - Set(float64) - // Inc increments the counter by 1. + // Inc increments the counter by 1. Use Add to increment it by arbitrary + // non-negative values. Inc() // Add adds the given value to the counter. It panics if the value is < // 0. @@ -50,6 +46,14 @@ type Counter interface { type CounterOpts Opts // NewCounter creates a new Counter based on the provided CounterOpts. +// +// The returned implementation tracks the counter value in two separate +// variables, a float64 and a uint64. The latter is used to track calls of the +// Inc method and calls of the Add method with a value that can be represented +// as a uint64. This allows atomic increments of the counter with optimal +// performance. (It is common to have an Inc call in very hot execution paths.) +// Both internal tracking values are added up in the Write method. This has to +// be taken into account when it comes to precision and overflow behavior. func NewCounter(opts CounterOpts) Counter { desc := NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), @@ -57,20 +61,58 @@ func NewCounter(opts CounterOpts) Counter { nil, opts.ConstLabels, ) - result := &counter{value: value{desc: desc, valType: CounterValue, labelPairs: desc.constLabelPairs}} + result := &counter{desc: desc, labelPairs: desc.constLabelPairs} result.init(result) // Init self-collection. return result } type counter struct { - value + // valBits contains the bits of the represented float64 value, while + // valInt stores values that are exact integers. Both have to go first + // in the struct to guarantee alignment for atomic operations. + // http://golang.org/pkg/sync/atomic/#pkg-note-BUG + valBits uint64 + valInt uint64 + + selfCollector + desc *Desc + + labelPairs []*dto.LabelPair +} + +func (c *counter) Desc() *Desc { + return c.desc } func (c *counter) Add(v float64) { if v < 0 { panic(errors.New("counter cannot decrease in value")) } - c.value.Add(v) + ival := uint64(v) + if float64(ival) == v { + atomic.AddUint64(&c.valInt, ival) + return + } + + for { + oldBits := atomic.LoadUint64(&c.valBits) + newBits := math.Float64bits(math.Float64frombits(oldBits) + v) + if atomic.CompareAndSwapUint64(&c.valBits, oldBits, newBits) { + return + } + } +} + +func (c *counter) Inc() { + atomic.AddUint64(&c.valInt, 1) +} + +func (c *counter) Write(out *dto.Metric) error { + fval := math.Float64frombits(atomic.LoadUint64(&c.valBits)) + ival := atomic.LoadUint64(&c.valInt) + val := fval + float64(ival) + + return populateMetric(CounterValue, val, c.labelPairs, out) } // CounterVec is a Collector that bundles a set of Counters that all share the @@ -78,16 +120,12 @@ func (c *counter) Add(v float64) { // if you want to count the same thing partitioned by various dimensions // (e.g. number of HTTP requests, partitioned by response code and // method). Create instances with NewCounterVec. -// -// CounterVec embeds MetricVec. See there for a full list of methods with -// detailed documentation. type CounterVec struct { - *MetricVec + *metricVec } // NewCounterVec creates a new CounterVec based on the provided CounterOpts and -// partitioned by the given label names. At least one label name must be -// provided. +// partitioned by the given label names. func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec { desc := NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), @@ -96,34 +134,62 @@ func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec { opts.ConstLabels, ) return &CounterVec{ - MetricVec: newMetricVec(desc, func(lvs ...string) Metric { - result := &counter{value: value{ - desc: desc, - valType: CounterValue, - labelPairs: makeLabelPairs(desc, lvs), - }} + metricVec: newMetricVec(desc, func(lvs ...string) Metric { + if len(lvs) != len(desc.variableLabels) { + panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, lvs)) + } + result := &counter{desc: desc, labelPairs: makeLabelPairs(desc, lvs)} result.init(result) // Init self-collection. return result }), } } -// GetMetricWithLabelValues replaces the method of the same name in -// MetricVec. The difference is that this method returns a Counter and not a -// Metric so that no type conversion is required. -func (m *CounterVec) GetMetricWithLabelValues(lvs ...string) (Counter, error) { - metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...) +// GetMetricWithLabelValues returns the Counter for the given slice of label +// values (same order as the VariableLabels in Desc). If that combination of +// label values is accessed for the first time, a new Counter is created. +// +// It is possible to call this method without using the returned Counter to only +// create the new Counter but leave it at its starting value 0. See also the +// SummaryVec example. +// +// Keeping the Counter for later use is possible (and should be considered if +// performance is critical), but keep in mind that Reset, DeleteLabelValues and +// Delete can be used to delete the Counter from the CounterVec. In that case, +// the Counter will still exist, but it will not be exported anymore, even if a +// Counter with the same label values is created later. +// +// An error is returned if the number of label values is not the same as the +// number of VariableLabels in Desc (minus any curried labels). +// +// Note that for more than one label value, this method is prone to mistakes +// caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as +// an alternative to avoid that type of mistake. For higher label numbers, the +// latter has a much more readable (albeit more verbose) syntax, but it comes +// with a performance overhead (for creating and processing the Labels map). +// See also the GaugeVec example. +func (v *CounterVec) GetMetricWithLabelValues(lvs ...string) (Counter, error) { + metric, err := v.metricVec.getMetricWithLabelValues(lvs...) if metric != nil { return metric.(Counter), err } return nil, err } -// GetMetricWith replaces the method of the same name in MetricVec. The -// difference is that this method returns a Counter and not a Metric so that no -// type conversion is required. -func (m *CounterVec) GetMetricWith(labels Labels) (Counter, error) { - metric, err := m.MetricVec.GetMetricWith(labels) +// GetMetricWith returns the Counter for the given Labels map (the label names +// must match those of the VariableLabels in Desc). If that label map is +// accessed for the first time, a new Counter is created. Implications of +// creating a Counter without using it and keeping the Counter for later use are +// the same as for GetMetricWithLabelValues. +// +// An error is returned if the number and names of the Labels are inconsistent +// with those of the VariableLabels in Desc (minus any curried labels). +// +// This method is used for the same purpose as +// GetMetricWithLabelValues(...string). See there for pros and cons of the two +// methods. +func (v *CounterVec) GetMetricWith(labels Labels) (Counter, error) { + metric, err := v.metricVec.getMetricWith(labels) if metric != nil { return metric.(Counter), err } @@ -131,18 +197,57 @@ func (m *CounterVec) GetMetricWith(labels Labels) (Counter, error) { } // WithLabelValues works as GetMetricWithLabelValues, but panics where -// GetMetricWithLabelValues would have returned an error. By not returning an -// error, WithLabelValues allows shortcuts like +// GetMetricWithLabelValues would have returned an error. Not returning an +// error allows shortcuts like // myVec.WithLabelValues("404", "GET").Add(42) -func (m *CounterVec) WithLabelValues(lvs ...string) Counter { - return m.MetricVec.WithLabelValues(lvs...).(Counter) +func (v *CounterVec) WithLabelValues(lvs ...string) Counter { + c, err := v.GetMetricWithLabelValues(lvs...) + if err != nil { + panic(err) + } + return c } // With works as GetMetricWith, but panics where GetMetricWithLabels would have -// returned an error. By not returning an error, With allows shortcuts like -// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42) -func (m *CounterVec) With(labels Labels) Counter { - return m.MetricVec.With(labels).(Counter) +// returned an error. Not returning an error allows shortcuts like +// myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Add(42) +func (v *CounterVec) With(labels Labels) Counter { + c, err := v.GetMetricWith(labels) + if err != nil { + panic(err) + } + return c +} + +// CurryWith returns a vector curried with the provided labels, i.e. the +// returned vector has those labels pre-set for all labeled operations performed +// on it. The cardinality of the curried vector is reduced accordingly. The +// order of the remaining labels stays the same (just with the curried labels +// taken out of the sequence – which is relevant for the +// (GetMetric)WithLabelValues methods). It is possible to curry a curried +// vector, but only with labels not yet used for currying before. +// +// The metrics contained in the CounterVec are shared between the curried and +// uncurried vectors. They are just accessed differently. Curried and uncurried +// vectors behave identically in terms of collection. Only one must be +// registered with a given registry (usually the uncurried version). The Reset +// method deletes all metrics, even if called on a curried vector. +func (v *CounterVec) CurryWith(labels Labels) (*CounterVec, error) { + vec, err := v.curryWith(labels) + if vec != nil { + return &CounterVec{vec}, err + } + return nil, err +} + +// MustCurryWith works as CurryWith but panics where CurryWith would have +// returned an error. +func (v *CounterVec) MustCurryWith(labels Labels) *CounterVec { + vec, err := v.CurryWith(labels) + if err != nil { + panic(err) + } + return vec } // CounterFunc is a Counter whose value is determined at collect time by calling a diff --git a/vendor/github.com/prometheus/client_golang/prometheus/counter_test.go b/vendor/github.com/prometheus/client_golang/prometheus/counter_test.go index 67391a23aa..5062f51af0 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/counter_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/counter_test.go @@ -14,6 +14,7 @@ package prometheus import ( + "fmt" "math" "testing" @@ -27,13 +28,27 @@ func TestCounterAdd(t *testing.T) { ConstLabels: Labels{"a": "1", "b": "2"}, }).(*counter) counter.Inc() - if expected, got := 1., math.Float64frombits(counter.valBits); expected != got { + if expected, got := 0.0, math.Float64frombits(counter.valBits); expected != got { t.Errorf("Expected %f, got %f.", expected, got) } + if expected, got := uint64(1), counter.valInt; expected != got { + t.Errorf("Expected %d, got %d.", expected, got) + } counter.Add(42) - if expected, got := 43., math.Float64frombits(counter.valBits); expected != got { + if expected, got := 0.0, math.Float64frombits(counter.valBits); expected != got { t.Errorf("Expected %f, got %f.", expected, got) } + if expected, got := uint64(43), counter.valInt; expected != got { + t.Errorf("Expected %d, got %d.", expected, got) + } + + counter.Add(24.42) + if expected, got := 24.42, math.Float64frombits(counter.valBits); expected != got { + t.Errorf("Expected %f, got %f.", expected, got) + } + if expected, got := uint64(43), counter.valInt; expected != got { + t.Errorf("Expected %d, got %d.", expected, got) + } if expected, got := "counter cannot decrease in value", decreaseCounter(counter).Error(); expected != got { t.Errorf("Expected error %q, got %q.", expected, got) @@ -42,7 +57,7 @@ func TestCounterAdd(t *testing.T) { m := &dto.Metric{} counter.Write(m) - if expected, got := `label: label: counter: `, m.String(); expected != got { + if expected, got := `label: label: counter: `, m.String(); expected != got { t.Errorf("expected %q, got %q", expected, got) } } @@ -56,3 +71,142 @@ func decreaseCounter(c *counter) (err error) { c.Add(-1) return nil } + +func TestCounterVecGetMetricWithInvalidLabelValues(t *testing.T) { + testCases := []struct { + desc string + labels Labels + }{ + { + desc: "non utf8 label value", + labels: Labels{"a": "\xFF"}, + }, + { + desc: "not enough label values", + labels: Labels{}, + }, + { + desc: "too many label values", + labels: Labels{"a": "1", "b": "2"}, + }, + } + + for _, test := range testCases { + counterVec := NewCounterVec(CounterOpts{ + Name: "test", + }, []string{"a"}) + + labelValues := make([]string, len(test.labels)) + for _, val := range test.labels { + labelValues = append(labelValues, val) + } + + expectPanic(t, func() { + counterVec.WithLabelValues(labelValues...) + }, fmt.Sprintf("WithLabelValues: expected panic because: %s", test.desc)) + expectPanic(t, func() { + counterVec.With(test.labels) + }, fmt.Sprintf("WithLabelValues: expected panic because: %s", test.desc)) + + if _, err := counterVec.GetMetricWithLabelValues(labelValues...); err == nil { + t.Errorf("GetMetricWithLabelValues: expected error because: %s", test.desc) + } + if _, err := counterVec.GetMetricWith(test.labels); err == nil { + t.Errorf("GetMetricWith: expected error because: %s", test.desc) + } + } +} + +func expectPanic(t *testing.T, op func(), errorMsg string) { + defer func() { + if err := recover(); err == nil { + t.Error(errorMsg) + } + }() + + op() +} + +func TestCounterAddInf(t *testing.T) { + counter := NewCounter(CounterOpts{ + Name: "test", + Help: "test help", + }).(*counter) + + counter.Inc() + if expected, got := 0.0, math.Float64frombits(counter.valBits); expected != got { + t.Errorf("Expected %f, got %f.", expected, got) + } + if expected, got := uint64(1), counter.valInt; expected != got { + t.Errorf("Expected %d, got %d.", expected, got) + } + + counter.Add(math.Inf(1)) + if expected, got := math.Inf(1), math.Float64frombits(counter.valBits); expected != got { + t.Errorf("valBits expected %f, got %f.", expected, got) + } + if expected, got := uint64(1), counter.valInt; expected != got { + t.Errorf("valInts expected %d, got %d.", expected, got) + } + + counter.Inc() + if expected, got := math.Inf(1), math.Float64frombits(counter.valBits); expected != got { + t.Errorf("Expected %f, got %f.", expected, got) + } + if expected, got := uint64(2), counter.valInt; expected != got { + t.Errorf("Expected %d, got %d.", expected, got) + } + + m := &dto.Metric{} + counter.Write(m) + + if expected, got := `counter: `, m.String(); expected != got { + t.Errorf("expected %q, got %q", expected, got) + } +} + +func TestCounterAddLarge(t *testing.T) { + counter := NewCounter(CounterOpts{ + Name: "test", + Help: "test help", + }).(*counter) + + // large overflows the underlying type and should therefore be stored in valBits. + large := float64(math.MaxUint64 + 1) + counter.Add(large) + if expected, got := large, math.Float64frombits(counter.valBits); expected != got { + t.Errorf("valBits expected %f, got %f.", expected, got) + } + if expected, got := uint64(0), counter.valInt; expected != got { + t.Errorf("valInts expected %d, got %d.", expected, got) + } + + m := &dto.Metric{} + counter.Write(m) + + if expected, got := fmt.Sprintf("counter: ", large), m.String(); expected != got { + t.Errorf("expected %q, got %q", expected, got) + } +} + +func TestCounterAddSmall(t *testing.T) { + counter := NewCounter(CounterOpts{ + Name: "test", + Help: "test help", + }).(*counter) + small := 0.000000000001 + counter.Add(small) + if expected, got := small, math.Float64frombits(counter.valBits); expected != got { + t.Errorf("valBits expected %f, got %f.", expected, got) + } + if expected, got := uint64(0), counter.valInt; expected != got { + t.Errorf("valInts expected %d, got %d.", expected, got) + } + + m := &dto.Metric{} + counter.Write(m) + + if expected, got := fmt.Sprintf("counter: ", small), m.String(); expected != got { + t.Errorf("expected %q, got %q", expected, got) + } +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/desc.go b/vendor/github.com/prometheus/client_golang/prometheus/desc.go index 77f4b30e84..1d034f871c 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/desc.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/desc.go @@ -16,33 +16,15 @@ package prometheus import ( "errors" "fmt" - "regexp" "sort" "strings" "github.com/golang/protobuf/proto" + "github.com/prometheus/common/model" dto "github.com/prometheus/client_model/go" ) -var ( - metricNameRE = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_:]*$`) - labelNameRE = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$") -) - -// reservedLabelPrefix is a prefix which is not legal in user-supplied -// label names. -const reservedLabelPrefix = "__" - -// Labels represents a collection of label name -> value mappings. This type is -// commonly used with the With(Labels) and GetMetricWith(Labels) methods of -// metric vector Collectors, e.g.: -// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42) -// -// The other use-case is the specification of constant label pairs in Opts or to -// create a Desc. -type Labels map[string]string - // Desc is the descriptor used by every Prometheus Metric. It is essentially // the immutable meta-data of a Metric. The normal Metric implementations // included in this package manage their Desc under the hood. Users only have to @@ -78,32 +60,27 @@ type Desc struct { // Help string. Each Desc with the same fqName must have the same // dimHash. dimHash uint64 - // err is an error that occured during construction. It is reported on + // err is an error that occurred during construction. It is reported on // registration time. err error } // NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc // and will be reported on registration time. variableLabels and constLabels can -// be nil if no such labels should be set. fqName and help must not be empty. +// be nil if no such labels should be set. fqName must not be empty. // // variableLabels only contain the label names. Their label values are variable // and therefore not part of the Desc. (They are managed within the Metric.) // // For constLabels, the label values are constant. Therefore, they are fully -// specified in the Desc. See the Opts documentation for the implications of -// constant labels. +// specified in the Desc. See the Collector example for a usage pattern. func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc { d := &Desc{ fqName: fqName, help: help, variableLabels: variableLabels, } - if help == "" { - d.err = errors.New("empty help string") - return d - } - if !metricNameRE.MatchString(fqName) { + if !model.IsValidMetricName(model.LabelValue(fqName)) { d.err = fmt.Errorf("%q is not a valid metric name", fqName) return d } @@ -116,7 +93,7 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) * // First add only the const label names and sort them... for labelName := range constLabels { if !checkLabelName(labelName) { - d.err = fmt.Errorf("%q is not a valid label name", labelName) + d.err = fmt.Errorf("%q is not a valid label name for metric %q", labelName, fqName) return d } labelNames = append(labelNames, labelName) @@ -127,12 +104,18 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) * for _, labelName := range labelNames { labelValues = append(labelValues, constLabels[labelName]) } + // Validate the const label values. They can't have a wrong cardinality, so + // use in len(labelValues) as expectedNumberOfValues. + if err := validateLabelValues(labelValues, len(labelValues)); err != nil { + d.err = err + return d + } // Now add the variable label names, but prefix them with something that // cannot be in a regular label name. That prevents matching the label // dimension with a different mix between preset and variable labels. for _, labelName := range variableLabels { if !checkLabelName(labelName) { - d.err = fmt.Errorf("%q is not a valid label name", labelName) + d.err = fmt.Errorf("%q is not a valid label name for metric %q", labelName, fqName) return d } labelNames = append(labelNames, "$"+labelName) @@ -142,6 +125,7 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) * d.err = errors.New("duplicate label names") return d } + vh := hashNew() for _, val := range labelValues { vh = hashAdd(vh, val) @@ -168,7 +152,7 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) * Value: proto.String(v), }) } - sort.Sort(LabelPairSorter(d.constLabelPairs)) + sort.Sort(labelPairSorter(d.constLabelPairs)) return d } @@ -198,8 +182,3 @@ func (d *Desc) String() string { d.variableLabels, ) } - -func checkLabelName(l string) bool { - return labelNameRE.MatchString(l) && - !strings.HasPrefix(l, reservedLabelPrefix) -} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/desc_test.go b/vendor/github.com/prometheus/client_golang/prometheus/desc_test.go new file mode 100644 index 0000000000..5f854db0bd --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/desc_test.go @@ -0,0 +1,30 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "testing" +) + +func TestNewDescInvalidLabelValues(t *testing.T) { + desc := NewDesc( + "sample_label", + "sample label", + nil, + Labels{"a": "\xFF"}, + ) + if desc.err == nil { + t.Errorf("NewDesc: expected error because: %s", desc.err) + } +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/doc.go b/vendor/github.com/prometheus/client_golang/prometheus/doc.go index b15a2d3b98..5d9525defc 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/doc.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/doc.go @@ -11,13 +11,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package prometheus provides metrics primitives to instrument code for -// monitoring. It also offers a registry for metrics. Sub-packages allow to -// expose the registered metrics via HTTP (package promhttp) or push them to a -// Pushgateway (package push). +// Package prometheus is the core instrumentation package. It provides metrics +// primitives to instrument code for monitoring. It also offers a registry for +// metrics. Sub-packages allow to expose the registered metrics via HTTP +// (package promhttp) or push them to a Pushgateway (package push). There is +// also a sub-package promauto, which provides metrics constructors with +// automatic registration. // // All exported functions and methods are safe to be used concurrently unless -//specified otherwise. +// specified otherwise. // // A Basic Example // @@ -26,6 +28,7 @@ // package main // // import ( +// "log" // "net/http" // // "github.com/prometheus/client_golang/prometheus" @@ -59,7 +62,7 @@ // // The Handler function provides a default handler to expose metrics // // via an HTTP server. "/metrics" is the usual endpoint for that. // http.Handle("/metrics", promhttp.Handler()) -// http.ListenAndServe(":8080", nil) +// log.Fatal(http.ListenAndServe(":8080", nil)) // } // // @@ -69,9 +72,12 @@ // Metrics // // The number of exported identifiers in this package might appear a bit -// overwhelming. Hovever, in addition to the basic plumbing shown in the example +// overwhelming. However, in addition to the basic plumbing shown in the example // above, you only need to understand the different metric types and their -// vector versions for basic usage. +// vector versions for basic usage. Furthermore, if you are not concerned with +// fine-grained control of when and how to register metrics with the registry, +// have a look at the promauto package, which will effectively allow you to +// ignore registration altogether in simple cases. // // Above, you have already touched the Counter and the Gauge. There are two more // advanced metric types: the Summary and Histogram. A more thorough description @@ -95,8 +101,8 @@ // SummaryVec, HistogramVec, and UntypedVec are not. // // To create instances of Metrics and their vector versions, you need a suitable -// …Opts struct, i.e. GaugeOpts, CounterOpts, SummaryOpts, -// HistogramOpts, or UntypedOpts. +// …Opts struct, i.e. GaugeOpts, CounterOpts, SummaryOpts, HistogramOpts, or +// UntypedOpts. // // Custom Collectors and constant Metrics // @@ -114,8 +120,18 @@ // Metric instances “on the fly” using NewConstMetric, NewConstHistogram, and // NewConstSummary (and their respective Must… versions). That will happen in // the Collect method. The Describe method has to return separate Desc -// instances, representative of the “throw-away” metrics to be created -// later. NewDesc comes in handy to create those Desc instances. +// instances, representative of the “throw-away” metrics to be created later. +// NewDesc comes in handy to create those Desc instances. Alternatively, you +// could return no Desc at all, which will marke the Collector “unchecked”. No +// checks are porformed at registration time, but metric consistency will still +// be ensured at scrape time, i.e. any inconsistencies will lead to scrape +// errors. Thus, with unchecked Collectors, the responsibility to not collect +// metrics that lead to inconsistencies in the total scrape result lies with the +// implementer of the Collector. While this is not a desirable state, it is +// sometimes necessary. The typical use case is a situatios where the exact +// metrics to be returned by a Collector cannot be predicted at registration +// time, but the implementer has sufficient knowledge of the whole system to +// guarantee metric consistency. // // The Collector example illustrates the use case. You can also look at the // source code of the processCollector (mirroring process metrics), the @@ -129,34 +145,34 @@ // Advanced Uses of the Registry // // While MustRegister is the by far most common way of registering a Collector, -// sometimes you might want to handle the errors the registration might -// cause. As suggested by the name, MustRegister panics if an error occurs. With -// the Register function, the error is returned and can be handled. +// sometimes you might want to handle the errors the registration might cause. +// As suggested by the name, MustRegister panics if an error occurs. With the +// Register function, the error is returned and can be handled. // // An error is returned if the registered Collector is incompatible or // inconsistent with already registered metrics. The registry aims for -// consistency of the collected metrics according to the Prometheus data -// model. Inconsistencies are ideally detected at registration time, not at -// collect time. The former will usually be detected at start-up time of a -// program, while the latter will only happen at scrape time, possibly not even -// on the first scrape if the inconsistency only becomes relevant later. That is -// the main reason why a Collector and a Metric have to describe themselves to -// the registry. +// consistency of the collected metrics according to the Prometheus data model. +// Inconsistencies are ideally detected at registration time, not at collect +// time. The former will usually be detected at start-up time of a program, +// while the latter will only happen at scrape time, possibly not even on the +// first scrape if the inconsistency only becomes relevant later. That is the +// main reason why a Collector and a Metric have to describe themselves to the +// registry. // // So far, everything we did operated on the so-called default registry, as it -// can be found in the global DefaultRegistry variable. With NewRegistry, you +// can be found in the global DefaultRegisterer variable. With NewRegistry, you // can create a custom registry, or you can even implement the Registerer or -// Gatherer interfaces yourself. The methods Register and Unregister work in -// the same way on a custom registry as the global functions Register and -// Unregister on the default registry. -// -// There are a number of uses for custom registries: You can use registries -// with special properties, see NewPedanticRegistry. You can avoid global state, -// as it is imposed by the DefaultRegistry. You can use multiple registries at -// the same time to expose different metrics in different ways. You can use +// Gatherer interfaces yourself. The methods Register and Unregister work in the +// same way on a custom registry as the global functions Register and Unregister +// on the default registry. +// +// There are a number of uses for custom registries: You can use registries with +// special properties, see NewPedanticRegistry. You can avoid global state, as +// it is imposed by the DefaultRegisterer. You can use multiple registries at +// the same time to expose different metrics in different ways. You can use // separate registries for testing purposes. // -// Also note that the DefaultRegistry comes registered with a Collector for Go +// Also note that the DefaultRegisterer comes registered with a Collector for Go // runtime metrics (via NewGoCollector) and a Collector for process metrics (via // NewProcessCollector). With a custom registry, you are in control and decide // yourself about the Collectors to register. @@ -166,16 +182,20 @@ // The Registry implements the Gatherer interface. The caller of the Gather // method can then expose the gathered metrics in some way. Usually, the metrics // are served via HTTP on the /metrics endpoint. That's happening in the example -// above. The tools to expose metrics via HTTP are in the promhttp -// sub-package. (The top-level functions in the prometheus package are -// deprecated.) +// above. The tools to expose metrics via HTTP are in the promhttp sub-package. +// (The top-level functions in the prometheus package are deprecated.) // // Pushing to the Pushgateway // // Function for pushing to the Pushgateway can be found in the push sub-package. // +// Graphite Bridge +// +// Functions and examples to push metrics from a Gatherer to Graphite can be +// found in the graphite sub-package. +// // Other Means of Exposition // -// More ways of exposing metrics can easily be added. Sending metrics to -// Graphite would be an example that will soon be implemented. +// More ways of exposing metrics can easily be added by following the approaches +// of the existing implementations. package prometheus diff --git a/vendor/github.com/prometheus/client_golang/prometheus/example_clustermanager_test.go b/vendor/github.com/prometheus/client_golang/prometheus/example_clustermanager_test.go index 260c1b52de..92b61ca851 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/example_clustermanager_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/example_clustermanager_test.go @@ -13,22 +13,28 @@ package prometheus_test -import "github.com/prometheus/client_golang/prometheus" +import ( + "log" + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) // ClusterManager is an example for a system that might have been built without // Prometheus in mind. It models a central manager of jobs running in a -// cluster. To turn it into something that collects Prometheus metrics, we -// simply add the two methods required for the Collector interface. +// cluster. Thus, we implement a custom Collector called +// ClusterManagerCollector, which collects information from a ClusterManager +// using its provided methods and turns them into Prometheus Metrics for +// collection. // // An additional challenge is that multiple instances of the ClusterManager are // run within the same binary, each in charge of a different zone. We need to -// make use of ConstLabels to be able to register each ClusterManager instance -// with Prometheus. +// make use of wrapping Registerers to be able to register each +// ClusterManagerCollector instance with Prometheus. type ClusterManager struct { - Zone string - OOMCountDesc *prometheus.Desc - RAMUsageDesc *prometheus.Desc - // ... many more fields + Zone string + // Contains many more fields not listed in this example. } // ReallyExpensiveAssessmentOfTheSystemState is a mock for the data gathering a @@ -50,10 +56,30 @@ func (c *ClusterManager) ReallyExpensiveAssessmentOfTheSystemState() ( return } -// Describe simply sends the two Descs in the struct to the channel. -func (c *ClusterManager) Describe(ch chan<- *prometheus.Desc) { - ch <- c.OOMCountDesc - ch <- c.RAMUsageDesc +// ClusterManagerCollector implements the Collector interface. +type ClusterManagerCollector struct { + ClusterManager *ClusterManager +} + +// Descriptors used by the ClusterManagerCollector below. +var ( + oomCountDesc = prometheus.NewDesc( + "clustermanager_oom_crashes_total", + "Number of OOM crashes.", + []string{"host"}, nil, + ) + ramUsageDesc = prometheus.NewDesc( + "clustermanager_ram_usage_bytes", + "RAM usage as reported to the cluster manager.", + []string{"host"}, nil, + ) +) + +// Describe is implemented with DescribeByCollect. That's possible because the +// Collect method will always return the same two metrics with the same two +// descriptors. +func (cc ClusterManagerCollector) Describe(ch chan<- *prometheus.Desc) { + prometheus.DescribeByCollect(cc, ch) } // Collect first triggers the ReallyExpensiveAssessmentOfTheSystemState. Then it @@ -61,11 +87,11 @@ func (c *ClusterManager) Describe(ch chan<- *prometheus.Desc) { // // Note that Collect could be called concurrently, so we depend on // ReallyExpensiveAssessmentOfTheSystemState to be concurrency-safe. -func (c *ClusterManager) Collect(ch chan<- prometheus.Metric) { - oomCountByHost, ramUsageByHost := c.ReallyExpensiveAssessmentOfTheSystemState() +func (cc ClusterManagerCollector) Collect(ch chan<- prometheus.Metric) { + oomCountByHost, ramUsageByHost := cc.ClusterManager.ReallyExpensiveAssessmentOfTheSystemState() for host, oomCount := range oomCountByHost { ch <- prometheus.MustNewConstMetric( - c.OOMCountDesc, + oomCountDesc, prometheus.CounterValue, float64(oomCount), host, @@ -73,7 +99,7 @@ func (c *ClusterManager) Collect(ch chan<- prometheus.Metric) { } for host, ramUsage := range ramUsageByHost { ch <- prometheus.MustNewConstMetric( - c.RAMUsageDesc, + ramUsageDesc, prometheus.GaugeValue, ramUsage, host, @@ -81,38 +107,36 @@ func (c *ClusterManager) Collect(ch chan<- prometheus.Metric) { } } -// NewClusterManager creates the two Descs OOMCountDesc and RAMUsageDesc. Note -// that the zone is set as a ConstLabel. (It's different in each instance of the -// ClusterManager, but constant over the lifetime of an instance.) Then there is -// a variable label "host", since we want to partition the collected metrics by -// host. Since all Descs created in this way are consistent across instances, -// with a guaranteed distinction by the "zone" label, we can register different -// ClusterManager instances with the same registry. -func NewClusterManager(zone string) *ClusterManager { - return &ClusterManager{ +// NewClusterManager first creates a Prometheus-ignorant ClusterManager +// instance. Then, it creates a ClusterManagerCollector for the just created +// ClusterManager. Finally, it registers the ClusterManagerCollector with a +// wrapping Registerer that adds the zone as a label. In this way, the metrics +// collected by different ClusterManagerCollectors do not collide. +func NewClusterManager(zone string, reg prometheus.Registerer) *ClusterManager { + c := &ClusterManager{ Zone: zone, - OOMCountDesc: prometheus.NewDesc( - "clustermanager_oom_crashes_total", - "Number of OOM crashes.", - []string{"host"}, - prometheus.Labels{"zone": zone}, - ), - RAMUsageDesc: prometheus.NewDesc( - "clustermanager_ram_usage_bytes", - "RAM usage as reported to the cluster manager.", - []string{"host"}, - prometheus.Labels{"zone": zone}, - ), } + cc := ClusterManagerCollector{ClusterManager: c} + prometheus.WrapRegistererWith(prometheus.Labels{"zone": zone}, reg).MustRegister(cc) + return c } func ExampleCollector() { - workerDB := NewClusterManager("db") - workerCA := NewClusterManager("ca") - // Since we are dealing with custom Collector implementations, it might // be a good idea to try it out with a pedantic registry. reg := prometheus.NewPedanticRegistry() - reg.MustRegister(workerDB) - reg.MustRegister(workerCA) + + // Construct cluster managers. In real code, we would assign them to + // variables to then do something with them. + NewClusterManager("db", reg) + NewClusterManager("ca", reg) + + // Add the standard process and Go metrics to the custom registry. + reg.MustRegister( + prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}), + prometheus.NewGoCollector(), + ) + + http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{})) + log.Fatal(http.ListenAndServe(":8080", nil)) } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/example_timer_complex_test.go b/vendor/github.com/prometheus/client_golang/prometheus/example_timer_complex_test.go new file mode 100644 index 0000000000..c5e7de5e5e --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/example_timer_complex_test.go @@ -0,0 +1,71 @@ +// Copyright 2014 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus_test + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + // apiRequestDuration tracks the duration separate for each HTTP status + // class (1xx, 2xx, ...). This creates a fair amount of time series on + // the Prometheus server. Usually, you would track the duration of + // serving HTTP request without partitioning by outcome. Do something + // like this only if needed. Also note how only status classes are + // tracked, not every single status code. The latter would create an + // even larger amount of time series. Request counters partitioned by + // status code are usually OK as each counter only creates one time + // series. Histograms are way more expensive, so partition with care and + // only where you really need separate latency tracking. Partitioning by + // status class is only an example. In concrete cases, other partitions + // might make more sense. + apiRequestDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "api_request_duration_seconds", + Help: "Histogram for the request duration of the public API, partitioned by status class.", + Buckets: prometheus.ExponentialBuckets(0.1, 1.5, 5), + }, + []string{"status_class"}, + ) +) + +func handler(w http.ResponseWriter, r *http.Request) { + status := http.StatusOK + // The ObserverFunc gets called by the deferred ObserveDuration and + // decides which Histogram's Observe method is called. + timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { + switch { + case status >= 500: // Server error. + apiRequestDuration.WithLabelValues("5xx").Observe(v) + case status >= 400: // Client error. + apiRequestDuration.WithLabelValues("4xx").Observe(v) + case status >= 300: // Redirection. + apiRequestDuration.WithLabelValues("3xx").Observe(v) + case status >= 200: // Success. + apiRequestDuration.WithLabelValues("2xx").Observe(v) + default: // Informational. + apiRequestDuration.WithLabelValues("1xx").Observe(v) + } + })) + defer timer.ObserveDuration() + + // Handle the request. Set status accordingly. + // ... +} + +func ExampleTimer_complex() { + http.HandleFunc("/api", handler) +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/example_timer_gauge_test.go b/vendor/github.com/prometheus/client_golang/prometheus/example_timer_gauge_test.go new file mode 100644 index 0000000000..7184a0d1d9 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/example_timer_gauge_test.go @@ -0,0 +1,48 @@ +// Copyright 2014 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus_test + +import ( + "os" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + // If a function is called rarely (i.e. not more often than scrapes + // happen) or ideally only once (like in a batch job), it can make sense + // to use a Gauge for timing the function call. For timing a batch job + // and pushing the result to a Pushgateway, see also the comprehensive + // example in the push package. + funcDuration = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "example_function_duration_seconds", + Help: "Duration of the last call of an example function.", + }) +) + +func run() error { + // The Set method of the Gauge is used to observe the duration. + timer := prometheus.NewTimer(prometheus.ObserverFunc(funcDuration.Set)) + defer timer.ObserveDuration() + + // Do something. Return errors as encountered. The use of 'defer' above + // makes sure the function is still timed properly. + return nil +} + +func ExampleTimer_gauge() { + if err := run(); err != nil { + os.Exit(1) + } +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/example_timer_test.go b/vendor/github.com/prometheus/client_golang/prometheus/example_timer_test.go new file mode 100644 index 0000000000..bd86bb4720 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/example_timer_test.go @@ -0,0 +1,40 @@ +// Copyright 2014 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus_test + +import ( + "math/rand" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + requestDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ + Name: "example_request_duration_seconds", + Help: "Histogram for the runtime of a simple example function.", + Buckets: prometheus.LinearBuckets(0.01, 0.01, 10), + }) +) + +func ExampleTimer() { + // timer times this example function. It uses a Histogram, but a Summary + // would also work, as both implement Observer. Check out + // https://prometheus.io/docs/practices/histograms/ for differences. + timer := prometheus.NewTimer(requestDuration) + defer timer.ObserveDuration() + + // Do something here that takes time. + time.Sleep(time.Duration(rand.NormFloat64()*10000+50000) * time.Microsecond) +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/examples_test.go b/vendor/github.com/prometheus/client_golang/prometheus/examples_test.go index f87f21a8f4..a3824793f2 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/examples_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/examples_test.go @@ -19,13 +19,13 @@ import ( "math" "net/http" "runtime" - "sort" "strings" + "time" - dto "github.com/prometheus/client_model/go" + "github.com/golang/protobuf/proto" "github.com/prometheus/common/expfmt" - "github.com/golang/protobuf/proto" + dto "github.com/prometheus/client_model/go" "github.com/prometheus/client_golang/prometheus" ) @@ -89,37 +89,6 @@ func ExampleGaugeFunc() { // GaugeFunc 'goroutines_count' registered. } -func ExampleCounter() { - pushCounter := prometheus.NewCounter(prometheus.CounterOpts{ - Name: "repository_pushes", // Note: No help string... - }) - err := prometheus.Register(pushCounter) // ... so this will return an error. - if err != nil { - fmt.Println("Push counter couldn't be registered, no counting will happen:", err) - return - } - - // Try it once more, this time with a help string. - pushCounter = prometheus.NewCounter(prometheus.CounterOpts{ - Name: "repository_pushes", - Help: "Number of pushes to external repository.", - }) - err = prometheus.Register(pushCounter) - if err != nil { - fmt.Println("Push counter couldn't be registered AGAIN, no counting will happen:", err) - return - } - - pushComplete := make(chan struct{}) - // TODO: Start a goroutine that performs repository pushes and reports - // each completion via the channel. - for _ = range pushComplete { - pushCounter.Inc() - } - // Output: - // Push counter couldn't be registered, no counting will happen: descriptor Desc{fqName: "repository_pushes", help: "", constLabels: {}, variableLabels: []} is invalid: empty help string -} - func ExampleCounterVec() { httpReqs := prometheus.NewCounterVec( prometheus.CounterOpts{ @@ -167,19 +136,6 @@ func ExampleInstrumentHandler() { )) } -func ExampleLabelPairSorter() { - labelPairs := []*dto.LabelPair{ - &dto.LabelPair{Name: proto.String("status"), Value: proto.String("404")}, - &dto.LabelPair{Name: proto.String("method"), Value: proto.String("get")}, - } - - sort.Sort(prometheus.LabelPairSorter(labelPairs)) - - fmt.Println(labelPairs) - // Output: - // [name:"method" value:"get" name:"status" value:"404" ] -} - func ExampleRegister() { // Imagine you have a worker pool and want to count the tasks completed. taskCounter := prometheus.NewCounter(prometheus.CounterOpts{ @@ -326,7 +282,7 @@ func ExampleRegister() { // taskCounter unregistered. // taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", constLabels: {}, variableLabels: [worker_id]} has different label names or a different help string // taskCounterVec registered. - // Worker initialization failed: inconsistent label cardinality + // Worker initialization failed: inconsistent label cardinality: expected 1 label values but got 2 in []string{"42", "spurious arg"} // notMyCounter is nil. // taskCounterForWorker42 registered. // taskCounterForWorker2001 registered. @@ -334,8 +290,9 @@ func ExampleRegister() { func ExampleSummary() { temps := prometheus.NewSummary(prometheus.SummaryOpts{ - Name: "pond_temperature_celsius", - Help: "The temperature of the frog pond.", // Sorry, we can't measure how badly it smells. + Name: "pond_temperature_celsius", + Help: "The temperature of the frog pond.", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }) // Simulate some observations. @@ -372,8 +329,9 @@ func ExampleSummary() { func ExampleSummaryVec() { temps := prometheus.NewSummaryVec( prometheus.SummaryOpts{ - Name: "pond_temperature_celsius", - Help: "The temperature of the frog pond.", // Sorry, we can't measure how badly it smells. + Name: "pond_temperature_celsius", + Help: "The temperature of the frog pond.", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }, []string{"species"}, ) @@ -640,6 +598,7 @@ func ExampleAlreadyRegisteredError() { panic(err) } } + reqCounter.Inc() } func ExampleGatherers() { @@ -709,7 +668,7 @@ humidity_percent{location="inside"} 33.2 # HELP temperature_kelvin Temperature in Kelvin. # Duplicate metric: temperature_kelvin{location="outside"} 265.3 - # Wrong labels: + # Missing location label (note that this is undesirable but valid): temperature_kelvin 4.5 ` @@ -737,15 +696,47 @@ temperature_kelvin 4.5 // temperature_kelvin{location="outside"} 273.14 // temperature_kelvin{location="somewhere else"} 4.5 // ---------- - // 2 error(s) occurred: - // * collected metric temperature_kelvin label: gauge: was collected before with the same name and label values - // * collected metric temperature_kelvin gauge: has label dimensions inconsistent with previously collected metrics in the same metric family + // collected metric "temperature_kelvin" { label: gauge: } was collected before with the same name and label values // # HELP humidity_percent Humidity in %. // # TYPE humidity_percent gauge // humidity_percent{location="inside"} 33.2 // humidity_percent{location="outside"} 45.4 // # HELP temperature_kelvin Temperature in Kelvin. // # TYPE temperature_kelvin gauge + // temperature_kelvin 4.5 // temperature_kelvin{location="inside"} 298.44 // temperature_kelvin{location="outside"} 273.14 } + +func ExampleNewMetricWithTimestamp() { + desc := prometheus.NewDesc( + "temperature_kelvin", + "Current temperature in Kelvin.", + nil, nil, + ) + + // Create a constant gauge from values we got from an external + // temperature reporting system. Those values are reported with a slight + // delay, so we want to add the timestamp of the actual measurement. + temperatureReportedByExternalSystem := 298.15 + timeReportedByExternalSystem := time.Date(2009, time.November, 10, 23, 0, 0, 12345678, time.UTC) + s := prometheus.NewMetricWithTimestamp( + timeReportedByExternalSystem, + prometheus.MustNewConstMetric( + desc, prometheus.GaugeValue, temperatureReportedByExternalSystem, + ), + ) + + // Just for demonstration, let's check the state of the gauge by + // (ab)using its Write method (which is usually only used by Prometheus + // internally). + metric := &dto.Metric{} + s.Write(metric) + fmt.Println(proto.MarshalTextString(metric)) + + // Output: + // gauge: < + // value: 298.15 + // > + // timestamp_ms: 1257894000012 +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/expvar_collector_test.go b/vendor/github.com/prometheus/client_golang/prometheus/expvar_collector_test.go index 5d3128faed..6bcd9b692d 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/expvar_collector_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/expvar_collector_test.go @@ -24,7 +24,7 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -func ExampleExpvarCollector() { +func ExampleNewExpvarCollector() { expvarCollector := prometheus.NewExpvarCollector(map[string]*prometheus.Desc{ "memstats": prometheus.NewDesc( "expvar_memstats", @@ -78,7 +78,7 @@ func ExampleExpvarCollector() { close(metricChan) }() for m := range metricChan { - if strings.Index(m.Desc().String(), "expvar_memstats") == -1 { + if !strings.Contains(m.Desc().String(), "expvar_memstats") { metric.Reset() m.Write(&metric) metricStrings = append(metricStrings, metric.String()) diff --git a/vendor/github.com/prometheus/client_golang/prometheus/fnv.go b/vendor/github.com/prometheus/client_golang/prometheus/fnv.go index e3b67df8ac..3d383a735c 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/fnv.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/fnv.go @@ -1,3 +1,16 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package prometheus // Inline and byte-free variant of hash/fnv's fnv64a. diff --git a/vendor/github.com/prometheus/client_golang/prometheus/gauge.go b/vendor/github.com/prometheus/client_golang/prometheus/gauge.go index 8b70e5141d..71d406bd92 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/gauge.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/gauge.go @@ -13,6 +13,14 @@ package prometheus +import ( + "math" + "sync/atomic" + "time" + + dto "github.com/prometheus/client_model/go" +) + // Gauge is a Metric that represents a single numerical value that can // arbitrarily go up and down. // @@ -27,29 +35,95 @@ type Gauge interface { // Set sets the Gauge to an arbitrary value. Set(float64) - // Inc increments the Gauge by 1. + // Inc increments the Gauge by 1. Use Add to increment it by arbitrary + // values. Inc() - // Dec decrements the Gauge by 1. + // Dec decrements the Gauge by 1. Use Sub to decrement it by arbitrary + // values. Dec() - // Add adds the given value to the Gauge. (The value can be - // negative, resulting in a decrease of the Gauge.) + // Add adds the given value to the Gauge. (The value can be negative, + // resulting in a decrease of the Gauge.) Add(float64) // Sub subtracts the given value from the Gauge. (The value can be // negative, resulting in an increase of the Gauge.) Sub(float64) + + // SetToCurrentTime sets the Gauge to the current Unix time in seconds. + SetToCurrentTime() } // GaugeOpts is an alias for Opts. See there for doc comments. type GaugeOpts Opts // NewGauge creates a new Gauge based on the provided GaugeOpts. +// +// The returned implementation is optimized for a fast Set method. If you have a +// choice for managing the value of a Gauge via Set vs. Inc/Dec/Add/Sub, pick +// the former. For example, the Inc method of the returned Gauge is slower than +// the Inc method of a Counter returned by NewCounter. This matches the typical +// scenarios for Gauges and Counters, where the former tends to be Set-heavy and +// the latter Inc-heavy. func NewGauge(opts GaugeOpts) Gauge { - return newValue(NewDesc( + desc := NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), opts.Help, nil, opts.ConstLabels, - ), GaugeValue, 0) + ) + result := &gauge{desc: desc, labelPairs: desc.constLabelPairs} + result.init(result) // Init self-collection. + return result +} + +type gauge struct { + // valBits contains the bits of the represented float64 value. It has + // to go first in the struct to guarantee alignment for atomic + // operations. http://golang.org/pkg/sync/atomic/#pkg-note-BUG + valBits uint64 + + selfCollector + + desc *Desc + labelPairs []*dto.LabelPair +} + +func (g *gauge) Desc() *Desc { + return g.desc +} + +func (g *gauge) Set(val float64) { + atomic.StoreUint64(&g.valBits, math.Float64bits(val)) +} + +func (g *gauge) SetToCurrentTime() { + g.Set(float64(time.Now().UnixNano()) / 1e9) +} + +func (g *gauge) Inc() { + g.Add(1) +} + +func (g *gauge) Dec() { + g.Add(-1) +} + +func (g *gauge) Add(val float64) { + for { + oldBits := atomic.LoadUint64(&g.valBits) + newBits := math.Float64bits(math.Float64frombits(oldBits) + val) + if atomic.CompareAndSwapUint64(&g.valBits, oldBits, newBits) { + return + } + } +} + +func (g *gauge) Sub(val float64) { + g.Add(val * -1) +} + +func (g *gauge) Write(out *dto.Metric) error { + val := math.Float64frombits(atomic.LoadUint64(&g.valBits)) + return populateMetric(GaugeValue, val, g.labelPairs, out) } // GaugeVec is a Collector that bundles a set of Gauges that all share the same @@ -58,12 +132,11 @@ func NewGauge(opts GaugeOpts) Gauge { // (e.g. number of operations queued, partitioned by user and operation // type). Create instances with NewGaugeVec. type GaugeVec struct { - *MetricVec + *metricVec } // NewGaugeVec creates a new GaugeVec based on the provided GaugeOpts and -// partitioned by the given label names. At least one label name must be -// provided. +// partitioned by the given label names. func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec { desc := NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), @@ -72,28 +145,62 @@ func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec { opts.ConstLabels, ) return &GaugeVec{ - MetricVec: newMetricVec(desc, func(lvs ...string) Metric { - return newValue(desc, GaugeValue, 0, lvs...) + metricVec: newMetricVec(desc, func(lvs ...string) Metric { + if len(lvs) != len(desc.variableLabels) { + panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, lvs)) + } + result := &gauge{desc: desc, labelPairs: makeLabelPairs(desc, lvs)} + result.init(result) // Init self-collection. + return result }), } } -// GetMetricWithLabelValues replaces the method of the same name in -// MetricVec. The difference is that this method returns a Gauge and not a -// Metric so that no type conversion is required. -func (m *GaugeVec) GetMetricWithLabelValues(lvs ...string) (Gauge, error) { - metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...) +// GetMetricWithLabelValues returns the Gauge for the given slice of label +// values (same order as the VariableLabels in Desc). If that combination of +// label values is accessed for the first time, a new Gauge is created. +// +// It is possible to call this method without using the returned Gauge to only +// create the new Gauge but leave it at its starting value 0. See also the +// SummaryVec example. +// +// Keeping the Gauge for later use is possible (and should be considered if +// performance is critical), but keep in mind that Reset, DeleteLabelValues and +// Delete can be used to delete the Gauge from the GaugeVec. In that case, the +// Gauge will still exist, but it will not be exported anymore, even if a +// Gauge with the same label values is created later. See also the CounterVec +// example. +// +// An error is returned if the number of label values is not the same as the +// number of VariableLabels in Desc (minus any curried labels). +// +// Note that for more than one label value, this method is prone to mistakes +// caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as +// an alternative to avoid that type of mistake. For higher label numbers, the +// latter has a much more readable (albeit more verbose) syntax, but it comes +// with a performance overhead (for creating and processing the Labels map). +func (v *GaugeVec) GetMetricWithLabelValues(lvs ...string) (Gauge, error) { + metric, err := v.metricVec.getMetricWithLabelValues(lvs...) if metric != nil { return metric.(Gauge), err } return nil, err } -// GetMetricWith replaces the method of the same name in MetricVec. The -// difference is that this method returns a Gauge and not a Metric so that no -// type conversion is required. -func (m *GaugeVec) GetMetricWith(labels Labels) (Gauge, error) { - metric, err := m.MetricVec.GetMetricWith(labels) +// GetMetricWith returns the Gauge for the given Labels map (the label names +// must match those of the VariableLabels in Desc). If that label map is +// accessed for the first time, a new Gauge is created. Implications of +// creating a Gauge without using it and keeping the Gauge for later use are +// the same as for GetMetricWithLabelValues. +// +// An error is returned if the number and names of the Labels are inconsistent +// with those of the VariableLabels in Desc (minus any curried labels). +// +// This method is used for the same purpose as +// GetMetricWithLabelValues(...string). See there for pros and cons of the two +// methods. +func (v *GaugeVec) GetMetricWith(labels Labels) (Gauge, error) { + metric, err := v.metricVec.getMetricWith(labels) if metric != nil { return metric.(Gauge), err } @@ -101,18 +208,57 @@ func (m *GaugeVec) GetMetricWith(labels Labels) (Gauge, error) { } // WithLabelValues works as GetMetricWithLabelValues, but panics where -// GetMetricWithLabelValues would have returned an error. By not returning an -// error, WithLabelValues allows shortcuts like +// GetMetricWithLabelValues would have returned an error. Not returning an +// error allows shortcuts like // myVec.WithLabelValues("404", "GET").Add(42) -func (m *GaugeVec) WithLabelValues(lvs ...string) Gauge { - return m.MetricVec.WithLabelValues(lvs...).(Gauge) +func (v *GaugeVec) WithLabelValues(lvs ...string) Gauge { + g, err := v.GetMetricWithLabelValues(lvs...) + if err != nil { + panic(err) + } + return g } // With works as GetMetricWith, but panics where GetMetricWithLabels would have -// returned an error. By not returning an error, With allows shortcuts like -// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42) -func (m *GaugeVec) With(labels Labels) Gauge { - return m.MetricVec.With(labels).(Gauge) +// returned an error. Not returning an error allows shortcuts like +// myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Add(42) +func (v *GaugeVec) With(labels Labels) Gauge { + g, err := v.GetMetricWith(labels) + if err != nil { + panic(err) + } + return g +} + +// CurryWith returns a vector curried with the provided labels, i.e. the +// returned vector has those labels pre-set for all labeled operations performed +// on it. The cardinality of the curried vector is reduced accordingly. The +// order of the remaining labels stays the same (just with the curried labels +// taken out of the sequence – which is relevant for the +// (GetMetric)WithLabelValues methods). It is possible to curry a curried +// vector, but only with labels not yet used for currying before. +// +// The metrics contained in the GaugeVec are shared between the curried and +// uncurried vectors. They are just accessed differently. Curried and uncurried +// vectors behave identically in terms of collection. Only one must be +// registered with a given registry (usually the uncurried version). The Reset +// method deletes all metrics, even if called on a curried vector. +func (v *GaugeVec) CurryWith(labels Labels) (*GaugeVec, error) { + vec, err := v.curryWith(labels) + if vec != nil { + return &GaugeVec{vec}, err + } + return nil, err +} + +// MustCurryWith works as CurryWith but panics where CurryWith would have +// returned an error. +func (v *GaugeVec) MustCurryWith(labels Labels) *GaugeVec { + vec, err := v.CurryWith(labels) + if err != nil { + panic(err) + } + return vec } // GaugeFunc is a Gauge whose value is determined at collect time by calling a diff --git a/vendor/github.com/prometheus/client_golang/prometheus/gauge_test.go b/vendor/github.com/prometheus/client_golang/prometheus/gauge_test.go index 48cab46367..a2e3c14165 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/gauge_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/gauge_test.go @@ -19,6 +19,7 @@ import ( "sync" "testing" "testing/quick" + "time" dto "github.com/prometheus/client_model/go" ) @@ -82,7 +83,7 @@ func TestGaugeConcurrency(t *testing.T) { } start.Done() - if expected, got := <-result, math.Float64frombits(gge.(*value).valBits); math.Abs(expected-got) > 0.000001 { + if expected, got := <-result, math.Float64frombits(gge.(*gauge).valBits); math.Abs(expected-got) > 0.000001 { t.Fatalf("expected approx. %f, got %f", expected, got) return false } @@ -146,7 +147,7 @@ func TestGaugeVecConcurrency(t *testing.T) { start.Done() for i := range sStreams { - if expected, got := <-results[i], math.Float64frombits(gge.WithLabelValues(string('A'+i)).(*value).valBits); math.Abs(expected-got) > 0.000001 { + if expected, got := <-results[i], math.Float64frombits(gge.WithLabelValues(string('A'+i)).(*gauge).valBits); math.Abs(expected-got) > 0.000001 { t.Fatalf("expected approx. %f, got %f", expected, got) return false } @@ -180,3 +181,22 @@ func TestGaugeFunc(t *testing.T) { t.Errorf("expected %q, got %q", expected, got) } } + +func TestGaugeSetCurrentTime(t *testing.T) { + g := NewGauge(GaugeOpts{ + Name: "test_name", + Help: "test help", + }) + g.SetToCurrentTime() + unixTime := float64(time.Now().Unix()) + + m := &dto.Metric{} + g.Write(m) + + delta := unixTime - m.GetGauge().GetValue() + // This is just a smoke test to make sure SetToCurrentTime is not + // totally off. Tests with current time involved are hard... + if math.Abs(delta) > 5 { + t.Errorf("Gauge set to current time deviates from current time by more than 5s, delta is %f seconds", delta) + } +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/go_collector.go b/vendor/github.com/prometheus/client_golang/prometheus/go_collector.go index abc9d4ec40..ba3b9333ed 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/go_collector.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/go_collector.go @@ -1,3 +1,16 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package prometheus import ( @@ -8,26 +21,39 @@ import ( ) type goCollector struct { - goroutines Gauge - gcDesc *Desc + goroutinesDesc *Desc + threadsDesc *Desc + gcDesc *Desc + goInfoDesc *Desc // metrics to describe and collect metrics memStatsMetrics } -// NewGoCollector returns a collector which exports metrics about the current -// go process. +// NewGoCollector returns a collector which exports metrics about the current Go +// process. This includes memory stats. To collect those, runtime.ReadMemStats +// is called. This causes a stop-the-world, which is very short with Go1.9+ +// (~25µs). However, with older Go versions, the stop-the-world duration depends +// on the heap size and can be quite significant (~1.7 ms/GiB as per +// https://go-review.googlesource.com/c/go/+/34937). func NewGoCollector() Collector { return &goCollector{ - goroutines: NewGauge(GaugeOpts{ - Namespace: "go", - Name: "goroutines", - Help: "Number of goroutines that currently exist.", - }), + goroutinesDesc: NewDesc( + "go_goroutines", + "Number of goroutines that currently exist.", + nil, nil), + threadsDesc: NewDesc( + "go_threads", + "Number of OS threads created.", + nil, nil), gcDesc: NewDesc( "go_gc_duration_seconds", "A summary of the GC invocation durations.", nil, nil), + goInfoDesc: NewDesc( + "go_info", + "Information about the Go environment.", + nil, Labels{"version": runtime.Version()}), metrics: memStatsMetrics{ { desc: NewDesc( @@ -48,7 +74,7 @@ func NewGoCollector() Collector { }, { desc: NewDesc( memstatNamespace("sys_bytes"), - "Number of bytes obtained by system. Sum of all system allocations.", + "Number of bytes obtained from system.", nil, nil, ), eval: func(ms *runtime.MemStats) float64 { return float64(ms.Sys) }, @@ -111,12 +137,12 @@ func NewGoCollector() Collector { valType: GaugeValue, }, { desc: NewDesc( - memstatNamespace("heap_released_bytes_total"), - "Total number of heap bytes released to OS.", + memstatNamespace("heap_released_bytes"), + "Number of heap bytes released to OS.", nil, nil, ), eval: func(ms *runtime.MemStats) float64 { return float64(ms.HeapReleased) }, - valType: CounterValue, + valType: GaugeValue, }, { desc: NewDesc( memstatNamespace("heap_objects"), @@ -213,6 +239,14 @@ func NewGoCollector() Collector { ), eval: func(ms *runtime.MemStats) float64 { return float64(ms.LastGC) / 1e9 }, valType: GaugeValue, + }, { + desc: NewDesc( + memstatNamespace("gc_cpu_fraction"), + "The fraction of this program's available CPU time used by the GC since the program started.", + nil, nil, + ), + eval: func(ms *runtime.MemStats) float64 { return ms.GCCPUFraction }, + valType: GaugeValue, }, }, } @@ -224,9 +258,10 @@ func memstatNamespace(s string) string { // Describe returns all descriptions of the collector. func (c *goCollector) Describe(ch chan<- *Desc) { - ch <- c.goroutines.Desc() + ch <- c.goroutinesDesc + ch <- c.threadsDesc ch <- c.gcDesc - + ch <- c.goInfoDesc for _, i := range c.metrics { ch <- i.desc } @@ -234,8 +269,9 @@ func (c *goCollector) Describe(ch chan<- *Desc) { // Collect returns the current state of all metrics of the collector. func (c *goCollector) Collect(ch chan<- Metric) { - c.goroutines.Set(float64(runtime.NumGoroutine())) - ch <- c.goroutines + ch <- MustNewConstMetric(c.goroutinesDesc, GaugeValue, float64(runtime.NumGoroutine())) + n, _ := runtime.ThreadCreateProfile(nil) + ch <- MustNewConstMetric(c.threadsDesc, GaugeValue, float64(n)) var stats debug.GCStats stats.PauseQuantiles = make([]time.Duration, 5) @@ -246,7 +282,9 @@ func (c *goCollector) Collect(ch chan<- Metric) { quantiles[float64(idx+1)/float64(len(stats.PauseQuantiles)-1)] = pq.Seconds() } quantiles[0.0] = stats.PauseQuantiles[0].Seconds() - ch <- MustNewConstSummary(c.gcDesc, uint64(stats.NumGC), float64(stats.PauseTotal.Seconds()), quantiles) + ch <- MustNewConstSummary(c.gcDesc, uint64(stats.NumGC), stats.PauseTotal.Seconds(), quantiles) + + ch <- MustNewConstMetric(c.goInfoDesc, GaugeValue, 1) ms := &runtime.MemStats{} runtime.ReadMemStats(ms) diff --git a/vendor/github.com/prometheus/client_golang/prometheus/go_collector_test.go b/vendor/github.com/prometheus/client_golang/prometheus/go_collector_test.go index 9a8858cbd2..f93dcdcfce 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/go_collector_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/go_collector_test.go @@ -1,3 +1,16 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package prometheus import ( @@ -29,33 +42,37 @@ func TestGoCollector(t *testing.T) { for { select { - case metric := <-ch: - switch m := metric.(type) { - // Attention, this also catches Counter... - case Gauge: - pb := &dto.Metric{} - m.Write(pb) - if pb.GetGauge() == nil { - continue - } - - if old == -1 { - old = int(pb.GetGauge().GetValue()) - close(waitc) - continue - } + case m := <-ch: + // m can be Gauge or Counter, + // currently just test the go_goroutines Gauge + // and ignore others. + if m.Desc().fqName != "go_goroutines" { + continue + } + pb := &dto.Metric{} + m.Write(pb) + if pb.GetGauge() == nil { + continue + } - if diff := int(pb.GetGauge().GetValue()) - old; diff != 1 { - // TODO: This is flaky in highly concurrent situations. - t.Errorf("want 1 new goroutine, got %d", diff) - } + if old == -1 { + old = int(pb.GetGauge().GetValue()) + close(waitc) + continue + } - // GoCollector performs two sends per call. - // On line 27 we need to receive the second send - // to shut down cleanly. - <-ch - return + if diff := int(pb.GetGauge().GetValue()) - old; diff != 1 { + // TODO: This is flaky in highly concurrent situations. + t.Errorf("want 1 new goroutine, got %d", diff) } + + // GoCollector performs three sends per call. + // On line 27 we need to receive three more sends + // to shut down cleanly. + <-ch + <-ch + <-ch + return case <-time.After(1 * time.Second): t.Fatalf("expected collect timed out") } @@ -85,37 +102,33 @@ func TestGCCollector(t *testing.T) { for { select { case metric := <-ch: - switch m := metric.(type) { - case *constSummary, *value: - pb := &dto.Metric{} - m.Write(pb) - if pb.GetSummary() == nil { - continue - } - - if len(pb.GetSummary().Quantile) != 5 { - t.Errorf("expected 4 buckets, got %d", len(pb.GetSummary().Quantile)) - } - for idx, want := range []float64{0.0, 0.25, 0.5, 0.75, 1.0} { - if *pb.GetSummary().Quantile[idx].Quantile != want { - t.Errorf("bucket #%d is off, got %f, want %f", idx, *pb.GetSummary().Quantile[idx].Quantile, want) - } - } - if first { - first = false - oldGC = *pb.GetSummary().SampleCount - oldPause = *pb.GetSummary().SampleSum - close(waitc) - continue - } - if diff := *pb.GetSummary().SampleCount - oldGC; diff != 1 { - t.Errorf("want 1 new garbage collection run, got %d", diff) - } - if diff := *pb.GetSummary().SampleSum - oldPause; diff <= 0 { - t.Errorf("want moar pause, got %f", diff) + pb := &dto.Metric{} + metric.Write(pb) + if pb.GetSummary() == nil { + continue + } + if len(pb.GetSummary().Quantile) != 5 { + t.Errorf("expected 4 buckets, got %d", len(pb.GetSummary().Quantile)) + } + for idx, want := range []float64{0.0, 0.25, 0.5, 0.75, 1.0} { + if *pb.GetSummary().Quantile[idx].Quantile != want { + t.Errorf("bucket #%d is off, got %f, want %f", idx, *pb.GetSummary().Quantile[idx].Quantile, want) } - return } + if first { + first = false + oldGC = *pb.GetSummary().SampleCount + oldPause = *pb.GetSummary().SampleSum + close(waitc) + continue + } + if diff := *pb.GetSummary().SampleCount - oldGC; diff != 1 { + t.Errorf("want 1 new garbage collection run, got %d", diff) + } + if diff := *pb.GetSummary().SampleSum - oldPause; diff <= 0 { + t.Errorf("want moar pause, got %f", diff) + } + return case <-time.After(1 * time.Second): t.Fatalf("expected collect timed out") } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/graphite/bridge.go b/vendor/github.com/prometheus/client_golang/prometheus/graphite/bridge.go new file mode 100644 index 0000000000..466e2295df --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/graphite/bridge.go @@ -0,0 +1,282 @@ +// Copyright 2016 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package graphite provides a bridge to push Prometheus metrics to a Graphite +// server. +package graphite + +import ( + "bufio" + "errors" + "fmt" + "io" + "net" + "sort" + "time" + + "github.com/prometheus/common/expfmt" + "github.com/prometheus/common/model" + "golang.org/x/net/context" + + dto "github.com/prometheus/client_model/go" + + "github.com/prometheus/client_golang/prometheus" +) + +const ( + defaultInterval = 15 * time.Second + millisecondsPerSecond = 1000 +) + +// HandlerErrorHandling defines how a Handler serving metrics will handle +// errors. +type HandlerErrorHandling int + +// These constants cause handlers serving metrics to behave as described if +// errors are encountered. +const ( + // Ignore errors and try to push as many metrics to Graphite as possible. + ContinueOnError HandlerErrorHandling = iota + + // Abort the push to Graphite upon the first error encountered. + AbortOnError +) + +// Config defines the Graphite bridge config. +type Config struct { + // The url to push data to. Required. + URL string + + // The prefix for the pushed Graphite metrics. Defaults to empty string. + Prefix string + + // The interval to use for pushing data to Graphite. Defaults to 15 seconds. + Interval time.Duration + + // The timeout for pushing metrics to Graphite. Defaults to 15 seconds. + Timeout time.Duration + + // The Gatherer to use for metrics. Defaults to prometheus.DefaultGatherer. + Gatherer prometheus.Gatherer + + // The logger that messages are written to. Defaults to no logging. + Logger Logger + + // ErrorHandling defines how errors are handled. Note that errors are + // logged regardless of the configured ErrorHandling provided Logger + // is not nil. + ErrorHandling HandlerErrorHandling +} + +// Bridge pushes metrics to the configured Graphite server. +type Bridge struct { + url string + prefix string + interval time.Duration + timeout time.Duration + + errorHandling HandlerErrorHandling + logger Logger + + g prometheus.Gatherer +} + +// Logger is the minimal interface Bridge needs for logging. Note that +// log.Logger from the standard library implements this interface, and it is +// easy to implement by custom loggers, if they don't do so already anyway. +type Logger interface { + Println(v ...interface{}) +} + +// NewBridge returns a pointer to a new Bridge struct. +func NewBridge(c *Config) (*Bridge, error) { + b := &Bridge{} + + if c.URL == "" { + return nil, errors.New("missing URL") + } + b.url = c.URL + + if c.Gatherer == nil { + b.g = prometheus.DefaultGatherer + } else { + b.g = c.Gatherer + } + + if c.Logger != nil { + b.logger = c.Logger + } + + if c.Prefix != "" { + b.prefix = c.Prefix + } + + var z time.Duration + if c.Interval == z { + b.interval = defaultInterval + } else { + b.interval = c.Interval + } + + if c.Timeout == z { + b.timeout = defaultInterval + } else { + b.timeout = c.Timeout + } + + b.errorHandling = c.ErrorHandling + + return b, nil +} + +// Run starts the event loop that pushes Prometheus metrics to Graphite at the +// configured interval. +func (b *Bridge) Run(ctx context.Context) { + ticker := time.NewTicker(b.interval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := b.Push(); err != nil && b.logger != nil { + b.logger.Println("error pushing to Graphite:", err) + } + case <-ctx.Done(): + return + } + } +} + +// Push pushes Prometheus metrics to the configured Graphite server. +func (b *Bridge) Push() error { + mfs, err := b.g.Gather() + if err != nil || len(mfs) == 0 { + switch b.errorHandling { + case AbortOnError: + return err + case ContinueOnError: + if b.logger != nil { + b.logger.Println("continue on error:", err) + } + default: + panic("unrecognized error handling value") + } + } + + conn, err := net.DialTimeout("tcp", b.url, b.timeout) + if err != nil { + return err + } + defer conn.Close() + + return writeMetrics(conn, mfs, b.prefix, model.Now()) +} + +func writeMetrics(w io.Writer, mfs []*dto.MetricFamily, prefix string, now model.Time) error { + vec, err := expfmt.ExtractSamples(&expfmt.DecodeOptions{ + Timestamp: now, + }, mfs...) + if err != nil { + return err + } + + buf := bufio.NewWriter(w) + for _, s := range vec { + for _, c := range prefix { + if _, err := buf.WriteRune(c); err != nil { + return err + } + } + if err := buf.WriteByte('.'); err != nil { + return err + } + if err := writeMetric(buf, s.Metric); err != nil { + return err + } + if _, err := fmt.Fprintf(buf, " %g %d\n", s.Value, int64(s.Timestamp)/millisecondsPerSecond); err != nil { + return err + } + if err := buf.Flush(); err != nil { + return err + } + } + + return nil +} + +func writeMetric(buf *bufio.Writer, m model.Metric) error { + metricName, hasName := m[model.MetricNameLabel] + numLabels := len(m) - 1 + if !hasName { + numLabels = len(m) + } + + labelStrings := make([]string, 0, numLabels) + for label, value := range m { + if label != model.MetricNameLabel { + labelStrings = append(labelStrings, fmt.Sprintf("%s %s", string(label), string(value))) + } + } + + var err error + switch numLabels { + case 0: + if hasName { + return writeSanitized(buf, string(metricName)) + } + default: + sort.Strings(labelStrings) + if err = writeSanitized(buf, string(metricName)); err != nil { + return err + } + for _, s := range labelStrings { + if err = buf.WriteByte('.'); err != nil { + return err + } + if err = writeSanitized(buf, s); err != nil { + return err + } + } + } + return nil +} + +func writeSanitized(buf *bufio.Writer, s string) error { + prevUnderscore := false + + for _, c := range s { + c = replaceInvalidRune(c) + if c == '_' { + if prevUnderscore { + continue + } + prevUnderscore = true + } else { + prevUnderscore = false + } + if _, err := buf.WriteRune(c); err != nil { + return err + } + } + + return nil +} + +func replaceInvalidRune(c rune) rune { + if c == ' ' { + return '.' + } + if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == ':' || c == '-' || (c >= '0' && c <= '9')) { + return '_' + } + return c +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/graphite/bridge_test.go b/vendor/github.com/prometheus/client_golang/prometheus/graphite/bridge_test.go new file mode 100644 index 0000000000..471edfe405 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/graphite/bridge_test.go @@ -0,0 +1,338 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package graphite + +import ( + "bufio" + "bytes" + "fmt" + "io" + "log" + "net" + "os" + "regexp" + "testing" + "time" + + "github.com/prometheus/common/model" + "golang.org/x/net/context" + + "github.com/prometheus/client_golang/prometheus" +) + +func TestSanitize(t *testing.T) { + testCases := []struct { + in, out string + }{ + {in: "hello", out: "hello"}, + {in: "hE/l1o", out: "hE_l1o"}, + {in: "he,*ll(.o", out: "he_ll_o"}, + {in: "hello_there%^&", out: "hello_there_"}, + {in: "hell-.o", out: "hell-_o"}, + } + + var buf bytes.Buffer + w := bufio.NewWriter(&buf) + + for i, tc := range testCases { + if err := writeSanitized(w, tc.in); err != nil { + t.Fatalf("write failed: %v", err) + } + if err := w.Flush(); err != nil { + t.Fatalf("flush failed: %v", err) + } + + if want, got := tc.out, buf.String(); want != got { + t.Fatalf("test case index %d: got sanitized string %s, want %s", i, got, want) + } + + buf.Reset() + } +} + +func TestWriteSummary(t *testing.T) { + sumVec := prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: "name", + Help: "docstring", + ConstLabels: prometheus.Labels{"constname": "constvalue"}, + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"labelname"}, + ) + + sumVec.WithLabelValues("val1").Observe(float64(10)) + sumVec.WithLabelValues("val1").Observe(float64(20)) + sumVec.WithLabelValues("val1").Observe(float64(30)) + sumVec.WithLabelValues("val2").Observe(float64(20)) + sumVec.WithLabelValues("val2").Observe(float64(30)) + sumVec.WithLabelValues("val2").Observe(float64(40)) + + reg := prometheus.NewRegistry() + reg.MustRegister(sumVec) + + mfs, err := reg.Gather() + if err != nil { + t.Fatalf("error: %v", err) + } + + testCases := []struct { + prefix string + }{ + {prefix: "prefix"}, + {prefix: "pre/fix"}, + {prefix: "pre.fix"}, + } + + const want = `%s.name.constname.constvalue.labelname.val1.quantile.0_5 20 1477043 +%s.name.constname.constvalue.labelname.val1.quantile.0_9 30 1477043 +%s.name.constname.constvalue.labelname.val1.quantile.0_99 30 1477043 +%s.name_sum.constname.constvalue.labelname.val1 60 1477043 +%s.name_count.constname.constvalue.labelname.val1 3 1477043 +%s.name.constname.constvalue.labelname.val2.quantile.0_5 30 1477043 +%s.name.constname.constvalue.labelname.val2.quantile.0_9 40 1477043 +%s.name.constname.constvalue.labelname.val2.quantile.0_99 40 1477043 +%s.name_sum.constname.constvalue.labelname.val2 90 1477043 +%s.name_count.constname.constvalue.labelname.val2 3 1477043 +` + for i, tc := range testCases { + + now := model.Time(1477043083) + var buf bytes.Buffer + err = writeMetrics(&buf, mfs, tc.prefix, now) + if err != nil { + t.Fatalf("error: %v", err) + } + + wantWithPrefix := fmt.Sprintf(want, + tc.prefix, tc.prefix, tc.prefix, tc.prefix, tc.prefix, + tc.prefix, tc.prefix, tc.prefix, tc.prefix, tc.prefix, + ) + if got := buf.String(); wantWithPrefix != got { + t.Fatalf("test case index %d: wanted \n%s\n, got \n%s\n", i, wantWithPrefix, got) + } + } +} + +func TestWriteHistogram(t *testing.T) { + histVec := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "name", + Help: "docstring", + ConstLabels: prometheus.Labels{"constname": "constvalue"}, + Buckets: []float64{0.01, 0.02, 0.05, 0.1}, + }, + []string{"labelname"}, + ) + + histVec.WithLabelValues("val1").Observe(float64(10)) + histVec.WithLabelValues("val1").Observe(float64(20)) + histVec.WithLabelValues("val1").Observe(float64(30)) + histVec.WithLabelValues("val2").Observe(float64(20)) + histVec.WithLabelValues("val2").Observe(float64(30)) + histVec.WithLabelValues("val2").Observe(float64(40)) + + reg := prometheus.NewRegistry() + reg.MustRegister(histVec) + + mfs, err := reg.Gather() + if err != nil { + t.Fatalf("error: %v", err) + } + + now := model.Time(1477043083) + var buf bytes.Buffer + err = writeMetrics(&buf, mfs, "prefix", now) + if err != nil { + t.Fatalf("error: %v", err) + } + + want := `prefix.name_bucket.constname.constvalue.labelname.val1.le.0_01 0 1477043 +prefix.name_bucket.constname.constvalue.labelname.val1.le.0_02 0 1477043 +prefix.name_bucket.constname.constvalue.labelname.val1.le.0_05 0 1477043 +prefix.name_bucket.constname.constvalue.labelname.val1.le.0_1 0 1477043 +prefix.name_sum.constname.constvalue.labelname.val1 60 1477043 +prefix.name_count.constname.constvalue.labelname.val1 3 1477043 +prefix.name_bucket.constname.constvalue.labelname.val1.le._Inf 3 1477043 +prefix.name_bucket.constname.constvalue.labelname.val2.le.0_01 0 1477043 +prefix.name_bucket.constname.constvalue.labelname.val2.le.0_02 0 1477043 +prefix.name_bucket.constname.constvalue.labelname.val2.le.0_05 0 1477043 +prefix.name_bucket.constname.constvalue.labelname.val2.le.0_1 0 1477043 +prefix.name_sum.constname.constvalue.labelname.val2 90 1477043 +prefix.name_count.constname.constvalue.labelname.val2 3 1477043 +prefix.name_bucket.constname.constvalue.labelname.val2.le._Inf 3 1477043 +` + if got := buf.String(); want != got { + t.Fatalf("wanted \n%s\n, got \n%s\n", want, got) + } +} + +func TestToReader(t *testing.T) { + cntVec := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "name", + Help: "docstring", + ConstLabels: prometheus.Labels{"constname": "constvalue"}, + }, + []string{"labelname"}, + ) + cntVec.WithLabelValues("val1").Inc() + cntVec.WithLabelValues("val2").Inc() + + reg := prometheus.NewRegistry() + reg.MustRegister(cntVec) + + want := `prefix.name.constname.constvalue.labelname.val1 1 1477043 +prefix.name.constname.constvalue.labelname.val2 1 1477043 +` + mfs, err := reg.Gather() + if err != nil { + t.Fatalf("error: %v", err) + } + + now := model.Time(1477043083) + var buf bytes.Buffer + err = writeMetrics(&buf, mfs, "prefix", now) + if err != nil { + t.Fatalf("error: %v", err) + } + + if got := buf.String(); want != got { + t.Fatalf("wanted \n%s\n, got \n%s\n", want, got) + } +} + +func TestPush(t *testing.T) { + reg := prometheus.NewRegistry() + cntVec := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "name", + Help: "docstring", + ConstLabels: prometheus.Labels{"constname": "constvalue"}, + }, + []string{"labelname"}, + ) + cntVec.WithLabelValues("val1").Inc() + cntVec.WithLabelValues("val2").Inc() + reg.MustRegister(cntVec) + + host := "localhost" + port := ":56789" + b, err := NewBridge(&Config{ + URL: host + port, + Gatherer: reg, + Prefix: "prefix", + }) + if err != nil { + t.Fatalf("error creating bridge: %v", err) + } + + nmg, err := newMockGraphite(port) + if err != nil { + t.Fatalf("error creating mock graphite: %v", err) + } + defer nmg.Close() + + err = b.Push() + if err != nil { + t.Fatalf("error pushing: %v", err) + } + + wants := []string{ + "prefix.name.constname.constvalue.labelname.val1 1", + "prefix.name.constname.constvalue.labelname.val2 1", + } + + select { + case got := <-nmg.readc: + for _, want := range wants { + matched, err := regexp.MatchString(want, got) + if err != nil { + t.Fatalf("error pushing: %v", err) + } + if !matched { + t.Fatalf("missing metric:\nno match for %s received by server:\n%s", want, got) + } + } + return + case err := <-nmg.errc: + t.Fatalf("error reading push: %v", err) + case <-time.After(50 * time.Millisecond): + t.Fatalf("no result from graphite server") + } +} + +func newMockGraphite(port string) (*mockGraphite, error) { + readc := make(chan string) + errc := make(chan error) + ln, err := net.Listen("tcp", port) + if err != nil { + return nil, err + } + + go func() { + conn, err := ln.Accept() + if err != nil { + errc <- err + } + var b bytes.Buffer + io.Copy(&b, conn) + readc <- b.String() + }() + + return &mockGraphite{ + readc: readc, + errc: errc, + Listener: ln, + }, nil +} + +type mockGraphite struct { + readc chan string + errc chan error + + net.Listener +} + +func ExampleBridge() { + b, err := NewBridge(&Config{ + URL: "graphite.example.org:3099", + Gatherer: prometheus.DefaultGatherer, + Prefix: "prefix", + Interval: 15 * time.Second, + Timeout: 10 * time.Second, + ErrorHandling: AbortOnError, + Logger: log.New(os.Stdout, "graphite bridge: ", log.Lshortfile), + }) + if err != nil { + panic(err) + } + + go func() { + // Start something in a goroutine that uses metrics. + }() + + // Push initial metrics to Graphite. Fail fast if the push fails. + if err := b.Push(); err != nil { + panic(err) + } + + // Create a Context to control stopping the Run() loop that pushes + // metrics to Graphite. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Start pushing metrics to Graphite in the Run() loop. + b.Run(ctx) +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/histogram.go b/vendor/github.com/prometheus/client_golang/prometheus/histogram.go index 9719e8fac8..f88da707bc 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/histogram.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/histogram.go @@ -16,7 +16,9 @@ package prometheus import ( "fmt" "math" + "runtime" "sort" + "sync" "sync/atomic" "github.com/golang/protobuf/proto" @@ -108,8 +110,9 @@ func ExponentialBuckets(start, factor float64, count int) []float64 { } // HistogramOpts bundles the options for creating a Histogram metric. It is -// mandatory to set Name and Help to a non-empty string. All other fields are -// optional and can safely be left at their zero value. +// mandatory to set Name to a non-empty string. All other fields are optional +// and can safely be left at their zero value, although it is strongly +// encouraged to set a Help string. type HistogramOpts struct { // Namespace, Subsystem, and Name are components of the fully-qualified // name of the Histogram (created by joining these components with @@ -120,29 +123,22 @@ type HistogramOpts struct { Subsystem string Name string - // Help provides information about this Histogram. Mandatory! + // Help provides information about this Histogram. // // Metrics with the same fully-qualified name must have the same Help // string. Help string - // ConstLabels are used to attach fixed labels to this - // Histogram. Histograms with the same fully-qualified name must have the - // same label names in their ConstLabels. + // ConstLabels are used to attach fixed labels to this metric. Metrics + // with the same fully-qualified name must have the same label names in + // their ConstLabels. // - // Note that in most cases, labels have a value that varies during the - // lifetime of a process. Those labels are usually managed with a - // HistogramVec. ConstLabels serve only special purposes. One is for the - // special case where the value of a label does not change during the - // lifetime of a process, e.g. if the revision of the running binary is - // put into a label. Another, more advanced purpose is if more than one - // Collector needs to collect Histograms with the same fully-qualified - // name. In that case, those Summaries must differ in the values of - // their ConstLabels. See the Collector examples. - // - // If the value of a label never changes (not even between binaries), - // that label most likely should not be a label at all (but part of the - // metric name). + // ConstLabels are only used rarely. In particular, do not use them to + // attach the same labels to all your metrics. Those use cases are + // better covered by target labels set by the scraping Prometheus + // server, or by one specific metric (e.g. a build_info or a + // machine_role metric). See also + // https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels,-not-static-scraped-labels ConstLabels Labels // Buckets defines the buckets into which observations are counted. Each @@ -169,7 +165,7 @@ func NewHistogram(opts HistogramOpts) Histogram { func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogram { if len(desc.variableLabels) != len(labelValues) { - panic(errInconsistentCardinality) + panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, labelValues)) } for _, n := range desc.variableLabels { @@ -191,6 +187,7 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr desc: desc, upperBounds: opts.Buckets, labelPairs: makeLabelPairs(desc, labelValues), + counts: [2]*histogramCounts{&histogramCounts{}, &histogramCounts{}}, } for i, upperBound := range h.upperBounds { if i < len(h.upperBounds)-1 { @@ -207,28 +204,53 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr } } } - // Finally we know the final length of h.upperBounds and can make counts. - h.counts = make([]uint64, len(h.upperBounds)) + // Finally we know the final length of h.upperBounds and can make counts + // for both states: + h.counts[0].buckets = make([]uint64, len(h.upperBounds)) + h.counts[1].buckets = make([]uint64, len(h.upperBounds)) h.init(h) // Init self-collection. return h } -type histogram struct { +type histogramCounts struct { // sumBits contains the bits of the float64 representing the sum of all // observations. sumBits and count have to go first in the struct to // guarantee alignment for atomic operations. // http://golang.org/pkg/sync/atomic/#pkg-note-BUG sumBits uint64 count uint64 + buckets []uint64 +} - selfCollector - // Note that there is no mutex required. +type histogram struct { + // countAndHotIdx is a complicated one. For lock-free yet atomic + // observations, we need to save the total count of observations again, + // combined with the index of the currently-hot counts struct, so that + // we can perform the operation on both values atomically. The least + // significant bit defines the hot counts struct. The remaining 63 bits + // represent the total count of observations. This happens under the + // assumption that the 63bit count will never overflow. Rationale: An + // observations takes about 30ns. Let's assume it could happen in + // 10ns. Overflowing the counter will then take at least (2^63)*10ns, + // which is about 3000 years. + // + // This has to be first in the struct for 64bit alignment. See + // http://golang.org/pkg/sync/atomic/#pkg-note-BUG + countAndHotIdx uint64 - desc *Desc + selfCollector + desc *Desc + writeMtx sync.Mutex // Only used in the Write method. upperBounds []float64 - counts []uint64 + + // Two counts, one is "hot" for lock-free observations, the other is + // "cold" for writing out a dto.Metric. It has to be an array of + // pointers to guarantee 64bit alignment of the histogramCounts, see + // http://golang.org/pkg/sync/atomic/#pkg-note-BUG. + counts [2]*histogramCounts + hotIdx int // Index of currently-hot counts. Only used within Write. labelPairs []*dto.LabelPair } @@ -248,36 +270,113 @@ func (h *histogram) Observe(v float64) { // 100 buckets: 78.1 ns/op linear - binary 54.9 ns/op // 300 buckets: 154 ns/op linear - binary 61.6 ns/op i := sort.SearchFloat64s(h.upperBounds, v) - if i < len(h.counts) { - atomic.AddUint64(&h.counts[i], 1) + + // We increment h.countAndHotIdx by 2 so that the counter in the upper + // 63 bits gets incremented by 1. At the same time, we get the new value + // back, which we can use to find the currently-hot counts. + n := atomic.AddUint64(&h.countAndHotIdx, 2) + hotCounts := h.counts[n%2] + + if i < len(h.upperBounds) { + atomic.AddUint64(&hotCounts.buckets[i], 1) } - atomic.AddUint64(&h.count, 1) for { - oldBits := atomic.LoadUint64(&h.sumBits) + oldBits := atomic.LoadUint64(&hotCounts.sumBits) newBits := math.Float64bits(math.Float64frombits(oldBits) + v) - if atomic.CompareAndSwapUint64(&h.sumBits, oldBits, newBits) { + if atomic.CompareAndSwapUint64(&hotCounts.sumBits, oldBits, newBits) { break } } + // Increment count last as we take it as a signal that the observation + // is complete. + atomic.AddUint64(&hotCounts.count, 1) } func (h *histogram) Write(out *dto.Metric) error { - his := &dto.Histogram{} - buckets := make([]*dto.Bucket, len(h.upperBounds)) + var ( + his = &dto.Histogram{} + buckets = make([]*dto.Bucket, len(h.upperBounds)) + hotCounts, coldCounts *histogramCounts + count uint64 + ) + + // For simplicity, we mutex the rest of this method. It is not in the + // hot path, i.e. Observe is called much more often than Write. The + // complication of making Write lock-free isn't worth it. + h.writeMtx.Lock() + defer h.writeMtx.Unlock() - his.SampleSum = proto.Float64(math.Float64frombits(atomic.LoadUint64(&h.sumBits))) - his.SampleCount = proto.Uint64(atomic.LoadUint64(&h.count)) - var count uint64 + // This is a bit arcane, which is why the following spells out this if + // clause in English: + // + // If the currently-hot counts struct is #0, we atomically increment + // h.countAndHotIdx by 1 so that from now on Observe will use the counts + // struct #1. Furthermore, the atomic increment gives us the new value, + // which, in its most significant 63 bits, tells us the count of + // observations done so far up to and including currently ongoing + // observations still using the counts struct just changed from hot to + // cold. To have a normal uint64 for the count, we bitshift by 1 and + // save the result in count. We also set h.hotIdx to 1 for the next + // Write call, and we will refer to counts #1 as hotCounts and to counts + // #0 as coldCounts. + // + // If the currently-hot counts struct is #1, we do the corresponding + // things the other way round. We have to _decrement_ h.countAndHotIdx + // (which is a bit arcane in itself, as we have to express -1 with an + // unsigned int...). + if h.hotIdx == 0 { + count = atomic.AddUint64(&h.countAndHotIdx, 1) >> 1 + h.hotIdx = 1 + hotCounts = h.counts[1] + coldCounts = h.counts[0] + } else { + count = atomic.AddUint64(&h.countAndHotIdx, ^uint64(0)) >> 1 // Decrement. + h.hotIdx = 0 + hotCounts = h.counts[0] + coldCounts = h.counts[1] + } + + // Now we have to wait for the now-declared-cold counts to actually cool + // down, i.e. wait for all observations still using it to finish. That's + // the case once the count in the cold counts struct is the same as the + // one atomically retrieved from the upper 63bits of h.countAndHotIdx. + for { + if count == atomic.LoadUint64(&coldCounts.count) { + break + } + runtime.Gosched() // Let observations get work done. + } + + his.SampleCount = proto.Uint64(count) + his.SampleSum = proto.Float64(math.Float64frombits(atomic.LoadUint64(&coldCounts.sumBits))) + var cumCount uint64 for i, upperBound := range h.upperBounds { - count += atomic.LoadUint64(&h.counts[i]) + cumCount += atomic.LoadUint64(&coldCounts.buckets[i]) buckets[i] = &dto.Bucket{ - CumulativeCount: proto.Uint64(count), + CumulativeCount: proto.Uint64(cumCount), UpperBound: proto.Float64(upperBound), } } + his.Bucket = buckets out.Histogram = his out.Label = h.labelPairs + + // Finally add all the cold counts to the new hot counts and reset the cold counts. + atomic.AddUint64(&hotCounts.count, count) + atomic.StoreUint64(&coldCounts.count, 0) + for { + oldBits := atomic.LoadUint64(&hotCounts.sumBits) + newBits := math.Float64bits(math.Float64frombits(oldBits) + his.GetSampleSum()) + if atomic.CompareAndSwapUint64(&hotCounts.sumBits, oldBits, newBits) { + atomic.StoreUint64(&coldCounts.sumBits, 0) + break + } + } + for i := range h.upperBounds { + atomic.AddUint64(&hotCounts.buckets[i], atomic.LoadUint64(&coldCounts.buckets[i])) + atomic.StoreUint64(&coldCounts.buckets[i], 0) + } return nil } @@ -287,12 +386,11 @@ func (h *histogram) Write(out *dto.Metric) error { // (e.g. HTTP request latencies, partitioned by status code and method). Create // instances with NewHistogramVec. type HistogramVec struct { - *MetricVec + *metricVec } // NewHistogramVec creates a new HistogramVec based on the provided HistogramOpts and -// partitioned by the given label names. At least one label name must be -// provided. +// partitioned by the given label names. func NewHistogramVec(opts HistogramOpts, labelNames []string) *HistogramVec { desc := NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), @@ -301,47 +399,116 @@ func NewHistogramVec(opts HistogramOpts, labelNames []string) *HistogramVec { opts.ConstLabels, ) return &HistogramVec{ - MetricVec: newMetricVec(desc, func(lvs ...string) Metric { + metricVec: newMetricVec(desc, func(lvs ...string) Metric { return newHistogram(desc, opts, lvs...) }), } } -// GetMetricWithLabelValues replaces the method of the same name in -// MetricVec. The difference is that this method returns a Histogram and not a -// Metric so that no type conversion is required. -func (m *HistogramVec) GetMetricWithLabelValues(lvs ...string) (Histogram, error) { - metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...) +// GetMetricWithLabelValues returns the Histogram for the given slice of label +// values (same order as the VariableLabels in Desc). If that combination of +// label values is accessed for the first time, a new Histogram is created. +// +// It is possible to call this method without using the returned Histogram to only +// create the new Histogram but leave it at its starting value, a Histogram without +// any observations. +// +// Keeping the Histogram for later use is possible (and should be considered if +// performance is critical), but keep in mind that Reset, DeleteLabelValues and +// Delete can be used to delete the Histogram from the HistogramVec. In that case, the +// Histogram will still exist, but it will not be exported anymore, even if a +// Histogram with the same label values is created later. See also the CounterVec +// example. +// +// An error is returned if the number of label values is not the same as the +// number of VariableLabels in Desc (minus any curried labels). +// +// Note that for more than one label value, this method is prone to mistakes +// caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as +// an alternative to avoid that type of mistake. For higher label numbers, the +// latter has a much more readable (albeit more verbose) syntax, but it comes +// with a performance overhead (for creating and processing the Labels map). +// See also the GaugeVec example. +func (v *HistogramVec) GetMetricWithLabelValues(lvs ...string) (Observer, error) { + metric, err := v.metricVec.getMetricWithLabelValues(lvs...) if metric != nil { - return metric.(Histogram), err + return metric.(Observer), err } return nil, err } -// GetMetricWith replaces the method of the same name in MetricVec. The -// difference is that this method returns a Histogram and not a Metric so that no -// type conversion is required. -func (m *HistogramVec) GetMetricWith(labels Labels) (Histogram, error) { - metric, err := m.MetricVec.GetMetricWith(labels) +// GetMetricWith returns the Histogram for the given Labels map (the label names +// must match those of the VariableLabels in Desc). If that label map is +// accessed for the first time, a new Histogram is created. Implications of +// creating a Histogram without using it and keeping the Histogram for later use +// are the same as for GetMetricWithLabelValues. +// +// An error is returned if the number and names of the Labels are inconsistent +// with those of the VariableLabels in Desc (minus any curried labels). +// +// This method is used for the same purpose as +// GetMetricWithLabelValues(...string). See there for pros and cons of the two +// methods. +func (v *HistogramVec) GetMetricWith(labels Labels) (Observer, error) { + metric, err := v.metricVec.getMetricWith(labels) if metric != nil { - return metric.(Histogram), err + return metric.(Observer), err } return nil, err } // WithLabelValues works as GetMetricWithLabelValues, but panics where -// GetMetricWithLabelValues would have returned an error. By not returning an -// error, WithLabelValues allows shortcuts like +// GetMetricWithLabelValues would have returned an error. Not returning an +// error allows shortcuts like // myVec.WithLabelValues("404", "GET").Observe(42.21) -func (m *HistogramVec) WithLabelValues(lvs ...string) Histogram { - return m.MetricVec.WithLabelValues(lvs...).(Histogram) +func (v *HistogramVec) WithLabelValues(lvs ...string) Observer { + h, err := v.GetMetricWithLabelValues(lvs...) + if err != nil { + panic(err) + } + return h +} + +// With works as GetMetricWith but panics where GetMetricWithLabels would have +// returned an error. Not returning an error allows shortcuts like +// myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Observe(42.21) +func (v *HistogramVec) With(labels Labels) Observer { + h, err := v.GetMetricWith(labels) + if err != nil { + panic(err) + } + return h +} + +// CurryWith returns a vector curried with the provided labels, i.e. the +// returned vector has those labels pre-set for all labeled operations performed +// on it. The cardinality of the curried vector is reduced accordingly. The +// order of the remaining labels stays the same (just with the curried labels +// taken out of the sequence – which is relevant for the +// (GetMetric)WithLabelValues methods). It is possible to curry a curried +// vector, but only with labels not yet used for currying before. +// +// The metrics contained in the HistogramVec are shared between the curried and +// uncurried vectors. They are just accessed differently. Curried and uncurried +// vectors behave identically in terms of collection. Only one must be +// registered with a given registry (usually the uncurried version). The Reset +// method deletes all metrics, even if called on a curried vector. +func (v *HistogramVec) CurryWith(labels Labels) (ObserverVec, error) { + vec, err := v.curryWith(labels) + if vec != nil { + return &HistogramVec{vec}, err + } + return nil, err } -// With works as GetMetricWith, but panics where GetMetricWithLabels would have -// returned an error. By not returning an error, With allows shortcuts like -// myVec.With(Labels{"code": "404", "method": "GET"}).Observe(42.21) -func (m *HistogramVec) With(labels Labels) Histogram { - return m.MetricVec.With(labels).(Histogram) +// MustCurryWith works as CurryWith but panics where CurryWith would have +// returned an error. +func (v *HistogramVec) MustCurryWith(labels Labels) ObserverVec { + vec, err := v.CurryWith(labels) + if err != nil { + panic(err) + } + return vec } type constHistogram struct { @@ -393,7 +560,7 @@ func (h *constHistogram) Write(out *dto.Metric) error { // bucket. // // NewConstHistogram returns an error if the length of labelValues is not -// consistent with the variable labels in Desc. +// consistent with the variable labels in Desc or if Desc is invalid. func NewConstHistogram( desc *Desc, count uint64, @@ -401,8 +568,11 @@ func NewConstHistogram( buckets map[float64]uint64, labelValues ...string, ) (Metric, error) { - if len(desc.variableLabels) != len(labelValues) { - return nil, errInconsistentCardinality + if desc.err != nil { + return nil, desc.err + } + if err := validateLabelValues(labelValues, len(desc.variableLabels)); err != nil { + return nil, err } return &constHistogram{ desc: desc, diff --git a/vendor/github.com/prometheus/client_golang/prometheus/histogram_test.go b/vendor/github.com/prometheus/client_golang/prometheus/histogram_test.go index d1242e08d6..9546b8776f 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/histogram_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/histogram_test.go @@ -17,6 +17,7 @@ import ( "math" "math/rand" "reflect" + "runtime" "sort" "sync" "testing" @@ -119,6 +120,28 @@ func BenchmarkHistogramWrite8(b *testing.B) { benchmarkHistogramWrite(8, b) } +func TestHistogramNonMonotonicBuckets(t *testing.T) { + testCases := map[string][]float64{ + "not strictly monotonic": {1, 2, 2, 3}, + "not monotonic at all": {1, 2, 4, 3, 5}, + "have +Inf in the middle": {1, 2, math.Inf(+1), 3}, + } + for name, buckets := range testCases { + func() { + defer func() { + if r := recover(); r == nil { + t.Errorf("Buckets %v are %s but NewHistogram did not panic.", buckets, name) + } + }() + _ = NewHistogram(HistogramOpts{ + Name: "test_histogram", + Help: "helpless", + Buckets: buckets, + }) + }() + } +} + // Intentionally adding +Inf here to test if that case is handled correctly. // Also, getCumulativeCounts depends on it. var testBuckets = []float64{-2, -1, -0.5, 0, 0.5, 1, 2, math.Inf(+1)} @@ -264,7 +287,7 @@ func TestHistogramVecConcurrency(t *testing.T) { for i := 0; i < vecLength; i++ { m := &dto.Metric{} s := his.WithLabelValues(string('A' + i)) - s.Write(m) + s.(Histogram).Write(m) if got, want := len(m.Histogram.Bucket), len(testBuckets)-1; got != want { t.Errorf("got %d buckets in protobuf, want %d", got, want) @@ -321,6 +344,50 @@ func TestBuckets(t *testing.T) { got = ExponentialBuckets(100, 1.2, 3) want = []float64{100, 120, 144} if !reflect.DeepEqual(got, want) { - t.Errorf("linear buckets: got %v, want %v", got, want) + t.Errorf("exponential buckets: got %v, want %v", got, want) + } +} + +func TestHistogramAtomicObserve(t *testing.T) { + var ( + quit = make(chan struct{}) + his = NewHistogram(HistogramOpts{ + Buckets: []float64{0.5, 10, 20}, + }) + ) + + defer func() { close(quit) }() + + observe := func() { + for { + select { + case <-quit: + return + default: + his.Observe(1) + } + } + } + + go observe() + go observe() + go observe() + + for i := 0; i < 100; i++ { + m := &dto.Metric{} + if err := his.Write(m); err != nil { + t.Fatal("unexpected error writing histogram:", err) + } + h := m.GetHistogram() + if h.GetSampleCount() != uint64(h.GetSampleSum()) || + h.GetSampleCount() != h.GetBucket()[1].GetCumulativeCount() || + h.GetSampleCount() != h.GetBucket()[2].GetCumulativeCount() { + t.Fatalf( + "inconsistent counts in histogram: count=%d sum=%f buckets=[%d, %d]", + h.GetSampleCount(), h.GetSampleSum(), + h.GetBucket()[1].GetCumulativeCount(), h.GetBucket()[2].GetCumulativeCount(), + ) + } + runtime.Gosched() } } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/http.go b/vendor/github.com/prometheus/client_golang/prometheus/http.go index 67ee5ac794..9f0875bfc8 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/http.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/http.go @@ -15,9 +15,7 @@ package prometheus import ( "bufio" - "bytes" "compress/gzip" - "fmt" "io" "net" "net/http" @@ -41,19 +39,10 @@ const ( acceptEncodingHeader = "Accept-Encoding" ) -var bufPool sync.Pool - -func getBuf() *bytes.Buffer { - buf := bufPool.Get() - if buf == nil { - return &bytes.Buffer{} - } - return buf.(*bytes.Buffer) -} - -func giveBuf(buf *bytes.Buffer) { - buf.Reset() - bufPool.Put(buf) +var gzipPool = sync.Pool{ + New: func() interface{} { + return gzip.NewWriter(nil) + }, } // Handler returns an HTTP handler for the DefaultGatherer. It is @@ -61,68 +50,50 @@ func giveBuf(buf *bytes.Buffer) { // name). // // Deprecated: Please note the issues described in the doc comment of -// InstrumentHandler. You might want to consider using promhttp.Handler instead -// (which is non instrumented). +// InstrumentHandler. You might want to consider using promhttp.Handler instead. func Handler() http.Handler { return InstrumentHandler("prometheus", UninstrumentedHandler()) } // UninstrumentedHandler returns an HTTP handler for the DefaultGatherer. // -// Deprecated: Use promhttp.Handler instead. See there for further documentation. +// Deprecated: Use promhttp.HandlerFor(DefaultGatherer, promhttp.HandlerOpts{}) +// instead. See there for further documentation. func UninstrumentedHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + return http.HandlerFunc(func(rsp http.ResponseWriter, req *http.Request) { mfs, err := DefaultGatherer.Gather() if err != nil { - http.Error(w, "An error has occurred during metrics collection:\n\n"+err.Error(), http.StatusInternalServerError) + httpError(rsp, err) return } contentType := expfmt.Negotiate(req.Header) - buf := getBuf() - defer giveBuf(buf) - writer, encoding := decorateWriter(req, buf) - enc := expfmt.NewEncoder(writer, contentType) - var lastErr error + header := rsp.Header() + header.Set(contentTypeHeader, string(contentType)) + + w := io.Writer(rsp) + if gzipAccepted(req.Header) { + header.Set(contentEncodingHeader, "gzip") + gz := gzipPool.Get().(*gzip.Writer) + defer gzipPool.Put(gz) + + gz.Reset(w) + defer gz.Close() + + w = gz + } + + enc := expfmt.NewEncoder(w, contentType) + for _, mf := range mfs { if err := enc.Encode(mf); err != nil { - lastErr = err - http.Error(w, "An error has occurred during metrics encoding:\n\n"+err.Error(), http.StatusInternalServerError) + httpError(rsp, err) return } } - if closer, ok := writer.(io.Closer); ok { - closer.Close() - } - if lastErr != nil && buf.Len() == 0 { - http.Error(w, "No metrics encoded, last error:\n\n"+err.Error(), http.StatusInternalServerError) - return - } - header := w.Header() - header.Set(contentTypeHeader, string(contentType)) - header.Set(contentLengthHeader, fmt.Sprint(buf.Len())) - if encoding != "" { - header.Set(contentEncodingHeader, encoding) - } - w.Write(buf.Bytes()) }) } -// decorateWriter wraps a writer to handle gzip compression if requested. It -// returns the decorated writer and the appropriate "Content-Encoding" header -// (which is empty if no compression is enabled). -func decorateWriter(request *http.Request, writer io.Writer) (io.Writer, string) { - header := request.Header.Get(acceptEncodingHeader) - parts := strings.Split(header, ",") - for _, part := range parts { - part := strings.TrimSpace(part) - if part == "gzip" || strings.HasPrefix(part, "gzip;") { - return gzip.NewWriter(writer), "gzip" - } - } - return writer, "" -} - var instLabels = []string{"method", "code"} type nower interface { @@ -139,16 +110,6 @@ var now nower = nowFunc(func() time.Time { return time.Now() }) -func nowSeries(t ...time.Time) nower { - return nowFunc(func() time.Time { - defer func() { - t = t[1:] - }() - - return t[0] - }) -} - // InstrumentHandler wraps the given HTTP handler for instrumentation. It // registers four metric collectors (if not already done) and reports HTTP // metrics to the (newly or already) registered collectors: http_requests_total @@ -158,23 +119,16 @@ func nowSeries(t ...time.Time) nower { // value. http_requests_total is a metric vector partitioned by HTTP method // (label name "method") and HTTP status code (label name "code"). // -// Deprecated: InstrumentHandler has several issues: -// -// - It uses Summaries rather than Histograms. Summaries are not useful if -// aggregation across multiple instances is required. -// -// - It uses microseconds as unit, which is deprecated and should be replaced by -// seconds. -// -// - The size of the request is calculated in a separate goroutine. Since this -// calculator requires access to the request header, it creates a race with -// any writes to the header performed during request handling. -// httputil.ReverseProxy is a prominent example for a handler -// performing such writes. -// -// Upcoming versions of this package will provide ways of instrumenting HTTP -// handlers that are more flexible and have fewer issues. Please prefer direct -// instrumentation in the meantime. +// Deprecated: InstrumentHandler has several issues. Use the tooling provided in +// package promhttp instead. The issues are the following: (1) It uses Summaries +// rather than Histograms. Summaries are not useful if aggregation across +// multiple instances is required. (2) It uses microseconds as unit, which is +// deprecated and should be replaced by seconds. (3) The size of the request is +// calculated in a separate goroutine. Since this calculator requires access to +// the request header, it creates a race with any writes to the header performed +// during request handling. httputil.ReverseProxy is a prominent example for a +// handler performing such writes. (4) It has additional issues with HTTP/2, cf. +// https://github.com/prometheus/client_golang/issues/272. func InstrumentHandler(handlerName string, handler http.Handler) http.HandlerFunc { return InstrumentHandlerFunc(handlerName, handler.ServeHTTP) } @@ -184,12 +138,13 @@ func InstrumentHandler(handlerName string, handler http.Handler) http.HandlerFun // issues). // // Deprecated: InstrumentHandlerFunc is deprecated for the same reasons as -// InstrumentHandler is. +// InstrumentHandler is. Use the tooling provided in package promhttp instead. func InstrumentHandlerFunc(handlerName string, handlerFunc func(http.ResponseWriter, *http.Request)) http.HandlerFunc { return InstrumentHandlerFuncWithOpts( SummaryOpts{ Subsystem: "http", ConstLabels: Labels{"handler": handlerName}, + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }, handlerFunc, ) @@ -222,7 +177,7 @@ func InstrumentHandlerFunc(handlerName string, handlerFunc func(http.ResponseWri // SummaryOpts. // // Deprecated: InstrumentHandlerWithOpts is deprecated for the same reasons as -// InstrumentHandler is. +// InstrumentHandler is. Use the tooling provided in package promhttp instead. func InstrumentHandlerWithOpts(opts SummaryOpts, handler http.Handler) http.HandlerFunc { return InstrumentHandlerFuncWithOpts(opts, handler.ServeHTTP) } @@ -233,7 +188,7 @@ func InstrumentHandlerWithOpts(opts SummaryOpts, handler http.Handler) http.Hand // SummaryOpts are used. // // Deprecated: InstrumentHandlerFuncWithOpts is deprecated for the same reasons -// as InstrumentHandler is. +// as InstrumentHandler is. Use the tooling provided in package promhttp instead. func InstrumentHandlerFuncWithOpts(opts SummaryOpts, handlerFunc func(http.ResponseWriter, *http.Request)) http.HandlerFunc { reqCnt := NewCounterVec( CounterOpts{ @@ -245,34 +200,52 @@ func InstrumentHandlerFuncWithOpts(opts SummaryOpts, handlerFunc func(http.Respo }, instLabels, ) + if err := Register(reqCnt); err != nil { + if are, ok := err.(AlreadyRegisteredError); ok { + reqCnt = are.ExistingCollector.(*CounterVec) + } else { + panic(err) + } + } opts.Name = "request_duration_microseconds" opts.Help = "The HTTP request latencies in microseconds." reqDur := NewSummary(opts) + if err := Register(reqDur); err != nil { + if are, ok := err.(AlreadyRegisteredError); ok { + reqDur = are.ExistingCollector.(Summary) + } else { + panic(err) + } + } opts.Name = "request_size_bytes" opts.Help = "The HTTP request sizes in bytes." reqSz := NewSummary(opts) + if err := Register(reqSz); err != nil { + if are, ok := err.(AlreadyRegisteredError); ok { + reqSz = are.ExistingCollector.(Summary) + } else { + panic(err) + } + } opts.Name = "response_size_bytes" opts.Help = "The HTTP response sizes in bytes." resSz := NewSummary(opts) - - regReqCnt := MustRegisterOrGet(reqCnt).(*CounterVec) - regReqDur := MustRegisterOrGet(reqDur).(Summary) - regReqSz := MustRegisterOrGet(reqSz).(Summary) - regResSz := MustRegisterOrGet(resSz).(Summary) + if err := Register(resSz); err != nil { + if are, ok := err.(AlreadyRegisteredError); ok { + resSz = are.ExistingCollector.(Summary) + } else { + panic(err) + } + } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { now := time.Now() delegate := &responseWriterDelegator{ResponseWriter: w} - out := make(chan int) - urlLen := 0 - if r.URL != nil { - urlLen = len(r.URL.String()) - } - go computeApproximateRequestSize(r, out, urlLen) + out := computeApproximateRequestSize(r) _, cn := w.(http.CloseNotifier) _, fl := w.(http.Flusher) @@ -290,39 +263,52 @@ func InstrumentHandlerFuncWithOpts(opts SummaryOpts, handlerFunc func(http.Respo method := sanitizeMethod(r.Method) code := sanitizeCode(delegate.status) - regReqCnt.WithLabelValues(method, code).Inc() - regReqDur.Observe(elapsed) - regResSz.Observe(float64(delegate.written)) - regReqSz.Observe(float64(<-out)) + reqCnt.WithLabelValues(method, code).Inc() + reqDur.Observe(elapsed) + resSz.Observe(float64(delegate.written)) + reqSz.Observe(float64(<-out)) }) } -func computeApproximateRequestSize(r *http.Request, out chan int, s int) { - s += len(r.Method) - s += len(r.Proto) - for name, values := range r.Header { - s += len(name) - for _, value := range values { - s += len(value) - } +func computeApproximateRequestSize(r *http.Request) <-chan int { + // Get URL length in current goroutine for avoiding a race condition. + // HandlerFunc that runs in parallel may modify the URL. + s := 0 + if r.URL != nil { + s += len(r.URL.String()) } - s += len(r.Host) - // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. + out := make(chan int, 1) - if r.ContentLength != -1 { - s += int(r.ContentLength) - } - out <- s + go func() { + s += len(r.Method) + s += len(r.Proto) + for name, values := range r.Header { + s += len(name) + for _, value := range values { + s += len(value) + } + } + s += len(r.Host) + + // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. + + if r.ContentLength != -1 { + s += int(r.ContentLength) + } + out <- s + close(out) + }() + + return out } type responseWriterDelegator struct { http.ResponseWriter - handler, method string - status int - written int64 - wroteHeader bool + status int + written int64 + wroteHeader bool } func (r *responseWriterDelegator) WriteHeader(code int) { @@ -488,3 +474,31 @@ func sanitizeCode(s int) string { return strconv.Itoa(s) } } + +// gzipAccepted returns whether the client will accept gzip-encoded content. +func gzipAccepted(header http.Header) bool { + a := header.Get(acceptEncodingHeader) + parts := strings.Split(a, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "gzip" || strings.HasPrefix(part, "gzip;") { + return true + } + } + return false +} + +// httpError removes any content-encoding header and then calls http.Error with +// the provided error and http.StatusInternalServerErrer. Error contents is +// supposed to be uncompressed plain text. However, same as with a plain +// http.Error, any header settings will be void if the header has already been +// sent. The error message will still be written to the writer, but it will +// probably be of limited use. +func httpError(rsp http.ResponseWriter, err error) { + rsp.Header().Del(contentEncodingHeader) + http.Error( + rsp, + "An error has occurred while serving metrics:\n\n"+err.Error(), + http.StatusInternalServerError, + ) +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/http_test.go b/vendor/github.com/prometheus/client_golang/prometheus/http_test.go index ffe0418cf8..dd89ccf1a6 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/http_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/http_test.go @@ -29,6 +29,16 @@ func (b respBody) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte(b)) } +func nowSeries(t ...time.Time) nower { + return nowFunc(func() time.Time { + defer func() { + t = t[1:] + }() + + return t[0] + }) +} + func TestInstrumentHandler(t *testing.T) { defer func(n nower) { now = n.(nower) @@ -37,16 +47,17 @@ func TestInstrumentHandler(t *testing.T) { instant := time.Now() end := instant.Add(30 * time.Second) now = nowSeries(instant, end) - respBody := respBody("Howdy there!") + body := respBody("Howdy there!") - hndlr := InstrumentHandler("test-handler", respBody) + hndlr := InstrumentHandler("test-handler", body) opts := SummaryOpts{ Subsystem: "http", ConstLabels: Labels{"handler": "test-handler"}, + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, } - reqCnt := MustRegisterOrGet(NewCounterVec( + reqCnt := NewCounterVec( CounterOpts{ Namespace: opts.Namespace, Subsystem: opts.Subsystem, @@ -55,19 +66,51 @@ func TestInstrumentHandler(t *testing.T) { ConstLabels: opts.ConstLabels, }, instLabels, - )).(*CounterVec) + ) + err := Register(reqCnt) + if err == nil { + t.Fatal("expected reqCnt to be registered already") + } + if are, ok := err.(AlreadyRegisteredError); ok { + reqCnt = are.ExistingCollector.(*CounterVec) + } else { + t.Fatal("unexpected registration error:", err) + } opts.Name = "request_duration_microseconds" opts.Help = "The HTTP request latencies in microseconds." - reqDur := MustRegisterOrGet(NewSummary(opts)).(Summary) + reqDur := NewSummary(opts) + err = Register(reqDur) + if err == nil { + t.Fatal("expected reqDur to be registered already") + } + if are, ok := err.(AlreadyRegisteredError); ok { + reqDur = are.ExistingCollector.(Summary) + } else { + t.Fatal("unexpected registration error:", err) + } opts.Name = "request_size_bytes" opts.Help = "The HTTP request sizes in bytes." - MustRegisterOrGet(NewSummary(opts)) + reqSz := NewSummary(opts) + err = Register(reqSz) + if err == nil { + t.Fatal("expected reqSz to be registered already") + } + if _, ok := err.(AlreadyRegisteredError); !ok { + t.Fatal("unexpected registration error:", err) + } opts.Name = "response_size_bytes" opts.Help = "The HTTP response sizes in bytes." - MustRegisterOrGet(NewSummary(opts)) + resSz := NewSummary(opts) + err = Register(resSz) + if err == nil { + t.Fatal("expected resSz to be registered already") + } + if _, ok := err.(AlreadyRegisteredError); !ok { + t.Fatal("unexpected registration error:", err) + } reqCnt.Reset() @@ -81,8 +124,8 @@ func TestInstrumentHandler(t *testing.T) { if resp.Code != http.StatusTeapot { t.Fatalf("expected status %d, got %d", http.StatusTeapot, resp.Code) } - if string(resp.Body.Bytes()) != "Howdy there!" { - t.Fatalf("expected body %s, got %s", "Howdy there!", string(resp.Body.Bytes())) + if resp.Body.String() != "Howdy there!" { + t.Fatalf("expected body %s, got %s", "Howdy there!", resp.Body.String()) } out := &dto.Metric{} @@ -95,7 +138,7 @@ func TestInstrumentHandler(t *testing.T) { } out.Reset() - if want, got := 1, len(reqCnt.children); want != got { + if want, got := 1, len(reqCnt.metricMap.metrics); want != got { t.Errorf("want %d children in reqCnt, got %d", want, got) } cnt, err := reqCnt.GetMetricWithLabelValues("get", "418") diff --git a/vendor/github.com/prometheus/client_golang/prometheus/internal/metric.go b/vendor/github.com/prometheus/client_golang/prometheus/internal/metric.go new file mode 100644 index 0000000000..351c26e1ae --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/internal/metric.go @@ -0,0 +1,85 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "sort" + + dto "github.com/prometheus/client_model/go" +) + +// metricSorter is a sortable slice of *dto.Metric. +type metricSorter []*dto.Metric + +func (s metricSorter) Len() int { + return len(s) +} + +func (s metricSorter) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s metricSorter) Less(i, j int) bool { + if len(s[i].Label) != len(s[j].Label) { + // This should not happen. The metrics are + // inconsistent. However, we have to deal with the fact, as + // people might use custom collectors or metric family injection + // to create inconsistent metrics. So let's simply compare the + // number of labels in this case. That will still yield + // reproducible sorting. + return len(s[i].Label) < len(s[j].Label) + } + for n, lp := range s[i].Label { + vi := lp.GetValue() + vj := s[j].Label[n].GetValue() + if vi != vj { + return vi < vj + } + } + + // We should never arrive here. Multiple metrics with the same + // label set in the same scrape will lead to undefined ingestion + // behavior. However, as above, we have to provide stable sorting + // here, even for inconsistent metrics. So sort equal metrics + // by their timestamp, with missing timestamps (implying "now") + // coming last. + if s[i].TimestampMs == nil { + return false + } + if s[j].TimestampMs == nil { + return true + } + return s[i].GetTimestampMs() < s[j].GetTimestampMs() +} + +// NormalizeMetricFamilies returns a MetricFamily slice with empty +// MetricFamilies pruned and the remaining MetricFamilies sorted by name within +// the slice, with the contained Metrics sorted within each MetricFamily. +func NormalizeMetricFamilies(metricFamiliesByName map[string]*dto.MetricFamily) []*dto.MetricFamily { + for _, mf := range metricFamiliesByName { + sort.Sort(metricSorter(mf.Metric)) + } + names := make([]string, 0, len(metricFamiliesByName)) + for name, mf := range metricFamiliesByName { + if len(mf.Metric) > 0 { + names = append(names, name) + } + } + sort.Strings(names) + result := make([]*dto.MetricFamily, 0, len(names)) + for _, name := range names { + result = append(result, metricFamiliesByName[name]) + } + return result +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/labels.go b/vendor/github.com/prometheus/client_golang/prometheus/labels.go new file mode 100644 index 0000000000..2744443ac2 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/labels.go @@ -0,0 +1,87 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "errors" + "fmt" + "strings" + "unicode/utf8" + + "github.com/prometheus/common/model" +) + +// Labels represents a collection of label name -> value mappings. This type is +// commonly used with the With(Labels) and GetMetricWith(Labels) methods of +// metric vector Collectors, e.g.: +// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42) +// +// The other use-case is the specification of constant label pairs in Opts or to +// create a Desc. +type Labels map[string]string + +// reservedLabelPrefix is a prefix which is not legal in user-supplied +// label names. +const reservedLabelPrefix = "__" + +var errInconsistentCardinality = errors.New("inconsistent label cardinality") + +func makeInconsistentCardinalityError(fqName string, labels, labelValues []string) error { + return fmt.Errorf( + "%s: %q has %d variable labels named %q but %d values %q were provided", + errInconsistentCardinality, fqName, + len(labels), labels, + len(labelValues), labelValues, + ) +} + +func validateValuesInLabels(labels Labels, expectedNumberOfValues int) error { + if len(labels) != expectedNumberOfValues { + return fmt.Errorf( + "%s: expected %d label values but got %d in %#v", + errInconsistentCardinality, expectedNumberOfValues, + len(labels), labels, + ) + } + + for name, val := range labels { + if !utf8.ValidString(val) { + return fmt.Errorf("label %s: value %q is not valid UTF-8", name, val) + } + } + + return nil +} + +func validateLabelValues(vals []string, expectedNumberOfValues int) error { + if len(vals) != expectedNumberOfValues { + return fmt.Errorf( + "%s: expected %d label values but got %d in %#v", + errInconsistentCardinality, expectedNumberOfValues, + len(vals), vals, + ) + } + + for _, val := range vals { + if !utf8.ValidString(val) { + return fmt.Errorf("label value %q is not valid UTF-8", val) + } + } + + return nil +} + +func checkLabelName(l string) bool { + return model.LabelName(l).IsValid() && !strings.HasPrefix(l, reservedLabelPrefix) +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/metric.go b/vendor/github.com/prometheus/client_golang/prometheus/metric.go index d4063d98f4..55e6d86d59 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/metric.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/metric.go @@ -15,6 +15,9 @@ package prometheus import ( "strings" + "time" + + "github.com/golang/protobuf/proto" dto "github.com/prometheus/client_model/go" ) @@ -43,9 +46,8 @@ type Metric interface { // While populating dto.Metric, it is the responsibility of the // implementation to ensure validity of the Metric protobuf (like valid // UTF-8 strings or syntactically valid metric and label names). It is - // recommended to sort labels lexicographically. (Implementers may find - // LabelPairSorter useful for that.) Callers of Write should still make - // sure of sorting if they depend on it. + // recommended to sort labels lexicographically. Callers of Write should + // still make sure of sorting if they depend on it. Write(*dto.Metric) error // TODO(beorn7): The original rationale of passing in a pre-allocated // dto.Metric protobuf to save allocations has disappeared. The @@ -57,8 +59,9 @@ type Metric interface { // implementation XXX has its own XXXOpts type, but in most cases, it is just be // an alias of this type (which might change when the requirement arises.) // -// It is mandatory to set Name and Help to a non-empty string. All other fields -// are optional and can safely be left at their zero value. +// It is mandatory to set Name to a non-empty string. All other fields are +// optional and can safely be left at their zero value, although it is strongly +// encouraged to set a Help string. type Opts struct { // Namespace, Subsystem, and Name are components of the fully-qualified // name of the Metric (created by joining these components with @@ -69,7 +72,7 @@ type Opts struct { Subsystem string Name string - // Help provides information about this metric. Mandatory! + // Help provides information about this metric. // // Metrics with the same fully-qualified name must have the same Help // string. @@ -79,20 +82,12 @@ type Opts struct { // with the same fully-qualified name must have the same label names in // their ConstLabels. // - // Note that in most cases, labels have a value that varies during the - // lifetime of a process. Those labels are usually managed with a metric - // vector collector (like CounterVec, GaugeVec, UntypedVec). ConstLabels - // serve only special purposes. One is for the special case where the - // value of a label does not change during the lifetime of a process, - // e.g. if the revision of the running binary is put into a - // label. Another, more advanced purpose is if more than one Collector - // needs to collect Metrics with the same fully-qualified name. In that - // case, those Metrics must differ in the values of their - // ConstLabels. See the Collector examples. - // - // If the value of a label never changes (not even between binaries), - // that label most likely should not be a label at all (but part of the - // metric name). + // ConstLabels are only used rarely. In particular, do not use them to + // attach the same labels to all your metrics. Those use cases are + // better covered by target labels set by the scraping Prometheus + // server, or by one specific metric (e.g. a build_info or a + // machine_role metric). See also + // https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels,-not-static-scraped-labels ConstLabels Labels } @@ -118,37 +113,22 @@ func BuildFQName(namespace, subsystem, name string) string { return name } -// LabelPairSorter implements sort.Interface. It is used to sort a slice of -// dto.LabelPair pointers. This is useful for implementing the Write method of -// custom metrics. -type LabelPairSorter []*dto.LabelPair +// labelPairSorter implements sort.Interface. It is used to sort a slice of +// dto.LabelPair pointers. +type labelPairSorter []*dto.LabelPair -func (s LabelPairSorter) Len() int { +func (s labelPairSorter) Len() int { return len(s) } -func (s LabelPairSorter) Swap(i, j int) { +func (s labelPairSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s LabelPairSorter) Less(i, j int) bool { +func (s labelPairSorter) Less(i, j int) bool { return s[i].GetName() < s[j].GetName() } -type hashSorter []uint64 - -func (s hashSorter) Len() int { - return len(s) -} - -func (s hashSorter) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -func (s hashSorter) Less(i, j int) bool { - return s[i] < s[j] -} - type invalidMetric struct { desc *Desc err error @@ -164,3 +144,31 @@ func NewInvalidMetric(desc *Desc, err error) Metric { func (m *invalidMetric) Desc() *Desc { return m.desc } func (m *invalidMetric) Write(*dto.Metric) error { return m.err } + +type timestampedMetric struct { + Metric + t time.Time +} + +func (m timestampedMetric) Write(pb *dto.Metric) error { + e := m.Metric.Write(pb) + pb.TimestampMs = proto.Int64(m.t.Unix()*1000 + int64(m.t.Nanosecond()/1000000)) + return e +} + +// NewMetricWithTimestamp returns a new Metric wrapping the provided Metric in a +// way that it has an explicit timestamp set to the provided Time. This is only +// useful in rare cases as the timestamp of a Prometheus metric should usually +// be set by the Prometheus server during scraping. Exceptions include mirroring +// metrics with given timestamps from other metric +// sources. +// +// NewMetricWithTimestamp works best with MustNewConstMetric, +// MustNewConstHistogram, and MustNewConstSummary, see example. +// +// Currently, the exposition formats used by Prometheus are limited to +// millisecond resolution. Thus, the provided time will be rounded down to the +// next full millisecond value. +func NewMetricWithTimestamp(t time.Time, m Metric) Metric { + return timestampedMetric{Metric: m, t: t} +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/observer.go b/vendor/github.com/prometheus/client_golang/prometheus/observer.go new file mode 100644 index 0000000000..5806cd09e3 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/observer.go @@ -0,0 +1,52 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +// Observer is the interface that wraps the Observe method, which is used by +// Histogram and Summary to add observations. +type Observer interface { + Observe(float64) +} + +// The ObserverFunc type is an adapter to allow the use of ordinary +// functions as Observers. If f is a function with the appropriate +// signature, ObserverFunc(f) is an Observer that calls f. +// +// This adapter is usually used in connection with the Timer type, and there are +// two general use cases: +// +// The most common one is to use a Gauge as the Observer for a Timer. +// See the "Gauge" Timer example. +// +// The more advanced use case is to create a function that dynamically decides +// which Observer to use for observing the duration. See the "Complex" Timer +// example. +type ObserverFunc func(float64) + +// Observe calls f(value). It implements Observer. +func (f ObserverFunc) Observe(value float64) { + f(value) +} + +// ObserverVec is an interface implemented by `HistogramVec` and `SummaryVec`. +type ObserverVec interface { + GetMetricWith(Labels) (Observer, error) + GetMetricWithLabelValues(lvs ...string) (Observer, error) + With(Labels) Observer + WithLabelValues(...string) Observer + CurryWith(Labels) (ObserverVec, error) + MustCurryWith(Labels) ObserverVec + + Collector +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/process_collector.go b/vendor/github.com/prometheus/client_golang/prometheus/process_collector.go index e31e62e78d..55176d58ce 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/process_collector.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/process_collector.go @@ -13,89 +13,139 @@ package prometheus -import "github.com/prometheus/procfs" +import ( + "errors" + "os" + + "github.com/prometheus/procfs" +) type processCollector struct { - pid int collectFn func(chan<- Metric) pidFn func() (int, error) - cpuTotal Counter - openFDs, maxFDs Gauge - vsize, rss Gauge - startTime Gauge + reportErrors bool + cpuTotal *Desc + openFDs, maxFDs *Desc + vsize, maxVsize *Desc + rss *Desc + startTime *Desc } -// NewProcessCollector returns a collector which exports the current state of -// process metrics including cpu, memory and file descriptor usage as well as -// the process start time for the given process id under the given namespace. -func NewProcessCollector(pid int, namespace string) Collector { - return NewProcessCollectorPIDFn( - func() (int, error) { return pid, nil }, - namespace, - ) +// ProcessCollectorOpts defines the behavior of a process metrics collector +// created with NewProcessCollector. +type ProcessCollectorOpts struct { + // PidFn returns the PID of the process the collector collects metrics + // for. It is called upon each collection. By default, the PID of the + // current process is used, as determined on construction time by + // calling os.Getpid(). + PidFn func() (int, error) + // If non-empty, each of the collected metrics is prefixed by the + // provided string and an underscore ("_"). + Namespace string + // If true, any error encountered during collection is reported as an + // invalid metric (see NewInvalidMetric). Otherwise, errors are ignored + // and the collected metrics will be incomplete. (Possibly, no metrics + // will be collected at all.) While that's usually not desired, it is + // appropriate for the common "mix-in" of process metrics, where process + // metrics are nice to have, but failing to collect them should not + // disrupt the collection of the remaining metrics. + ReportErrors bool } -// NewProcessCollectorPIDFn returns a collector which exports the current state -// of process metrics including cpu, memory and file descriptor usage as well -// as the process start time under the given namespace. The given pidFn is -// called on each collect and is used to determine the process to export -// metrics for. -func NewProcessCollectorPIDFn( - pidFn func() (int, error), - namespace string, -) Collector { - c := processCollector{ - pidFn: pidFn, - collectFn: func(chan<- Metric) {}, - - cpuTotal: NewCounter(CounterOpts{ - Namespace: namespace, - Name: "process_cpu_seconds_total", - Help: "Total user and system CPU time spent in seconds.", - }), - openFDs: NewGauge(GaugeOpts{ - Namespace: namespace, - Name: "process_open_fds", - Help: "Number of open file descriptors.", - }), - maxFDs: NewGauge(GaugeOpts{ - Namespace: namespace, - Name: "process_max_fds", - Help: "Maximum number of open file descriptors.", - }), - vsize: NewGauge(GaugeOpts{ - Namespace: namespace, - Name: "process_virtual_memory_bytes", - Help: "Virtual memory size in bytes.", - }), - rss: NewGauge(GaugeOpts{ - Namespace: namespace, - Name: "process_resident_memory_bytes", - Help: "Resident memory size in bytes.", - }), - startTime: NewGauge(GaugeOpts{ - Namespace: namespace, - Name: "process_start_time_seconds", - Help: "Start time of the process since unix epoch in seconds.", - }), +// NewProcessCollector returns a collector which exports the current state of +// process metrics including CPU, memory and file descriptor usage as well as +// the process start time. The detailed behavior is defined by the provided +// ProcessCollectorOpts. The zero value of ProcessCollectorOpts creates a +// collector for the current process with an empty namespace string and no error +// reporting. +// +// Currently, the collector depends on a Linux-style proc filesystem and +// therefore only exports metrics for Linux. +// +// Note: An older version of this function had the following signature: +// +// NewProcessCollector(pid int, namespace string) Collector +// +// Most commonly, it was called as +// +// NewProcessCollector(os.Getpid(), "") +// +// The following call of the current version is equivalent to the above: +// +// NewProcessCollector(ProcessCollectorOpts{}) +func NewProcessCollector(opts ProcessCollectorOpts) Collector { + ns := "" + if len(opts.Namespace) > 0 { + ns = opts.Namespace + "_" + } + + c := &processCollector{ + reportErrors: opts.ReportErrors, + cpuTotal: NewDesc( + ns+"process_cpu_seconds_total", + "Total user and system CPU time spent in seconds.", + nil, nil, + ), + openFDs: NewDesc( + ns+"process_open_fds", + "Number of open file descriptors.", + nil, nil, + ), + maxFDs: NewDesc( + ns+"process_max_fds", + "Maximum number of open file descriptors.", + nil, nil, + ), + vsize: NewDesc( + ns+"process_virtual_memory_bytes", + "Virtual memory size in bytes.", + nil, nil, + ), + maxVsize: NewDesc( + ns+"process_virtual_memory_max_bytes", + "Maximum amount of virtual memory available in bytes.", + nil, nil, + ), + rss: NewDesc( + ns+"process_resident_memory_bytes", + "Resident memory size in bytes.", + nil, nil, + ), + startTime: NewDesc( + ns+"process_start_time_seconds", + "Start time of the process since unix epoch in seconds.", + nil, nil, + ), + } + + if opts.PidFn == nil { + pid := os.Getpid() + c.pidFn = func() (int, error) { return pid, nil } + } else { + c.pidFn = opts.PidFn } // Set up process metric collection if supported by the runtime. if _, err := procfs.NewStat(); err == nil { c.collectFn = c.processCollect + } else { + c.collectFn = func(ch chan<- Metric) { + c.reportError(ch, nil, errors.New("process metrics not supported on this platform")) + } } - return &c + return c } // Describe returns all descriptions of the collector. func (c *processCollector) Describe(ch chan<- *Desc) { - ch <- c.cpuTotal.Desc() - ch <- c.openFDs.Desc() - ch <- c.maxFDs.Desc() - ch <- c.vsize.Desc() - ch <- c.rss.Desc() - ch <- c.startTime.Desc() + ch <- c.cpuTotal + ch <- c.openFDs + ch <- c.maxFDs + ch <- c.vsize + ch <- c.maxVsize + ch <- c.rss + ch <- c.startTime } // Collect returns the current state of all metrics of the collector. @@ -103,40 +153,52 @@ func (c *processCollector) Collect(ch chan<- Metric) { c.collectFn(ch) } -// TODO(ts): Bring back error reporting by reverting 7faf9e7 as soon as the -// client allows users to configure the error behavior. func (c *processCollector) processCollect(ch chan<- Metric) { pid, err := c.pidFn() if err != nil { + c.reportError(ch, nil, err) return } p, err := procfs.NewProc(pid) if err != nil { + c.reportError(ch, nil, err) return } if stat, err := p.NewStat(); err == nil { - c.cpuTotal.Set(stat.CPUTime()) - ch <- c.cpuTotal - c.vsize.Set(float64(stat.VirtualMemory())) - ch <- c.vsize - c.rss.Set(float64(stat.ResidentMemory())) - ch <- c.rss - + ch <- MustNewConstMetric(c.cpuTotal, CounterValue, stat.CPUTime()) + ch <- MustNewConstMetric(c.vsize, GaugeValue, float64(stat.VirtualMemory())) + ch <- MustNewConstMetric(c.rss, GaugeValue, float64(stat.ResidentMemory())) if startTime, err := stat.StartTime(); err == nil { - c.startTime.Set(startTime) - ch <- c.startTime + ch <- MustNewConstMetric(c.startTime, GaugeValue, startTime) + } else { + c.reportError(ch, c.startTime, err) } + } else { + c.reportError(ch, nil, err) } if fds, err := p.FileDescriptorsLen(); err == nil { - c.openFDs.Set(float64(fds)) - ch <- c.openFDs + ch <- MustNewConstMetric(c.openFDs, GaugeValue, float64(fds)) + } else { + c.reportError(ch, c.openFDs, err) } if limits, err := p.NewLimits(); err == nil { - c.maxFDs.Set(float64(limits.OpenFiles)) - ch <- c.maxFDs + ch <- MustNewConstMetric(c.maxFDs, GaugeValue, float64(limits.OpenFiles)) + ch <- MustNewConstMetric(c.maxVsize, GaugeValue, float64(limits.AddressSpace)) + } else { + c.reportError(ch, nil, err) + } +} + +func (c *processCollector) reportError(ch chan<- Metric, desc *Desc, err error) { + if !c.reportErrors { + return + } + if desc == nil { + desc = NewInvalidDesc(err) } + ch <- NewInvalidMetric(desc, err) } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/process_collector_test.go b/vendor/github.com/prometheus/client_golang/prometheus/process_collector_test.go index d3362dae72..8651d4f133 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/process_collector_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/process_collector_test.go @@ -1,13 +1,31 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build linux + package prometheus import ( "bytes" + "errors" "os" "regexp" "testing" "github.com/prometheus/common/expfmt" "github.com/prometheus/procfs" + + dto "github.com/prometheus/client_model/go" ) func TestProcessCollector(t *testing.T) { @@ -16,12 +34,14 @@ func TestProcessCollector(t *testing.T) { } registry := NewRegistry() - if err := registry.Register(NewProcessCollector(os.Getpid(), "")); err != nil { + if err := registry.Register(NewProcessCollector(ProcessCollectorOpts{})); err != nil { t.Fatal(err) } - if err := registry.Register(NewProcessCollectorPIDFn( - func() (int, error) { return os.Getpid(), nil }, "foobar"), - ); err != nil { + if err := registry.Register(NewProcessCollector(ProcessCollectorOpts{ + PidFn: func() (int, error) { return os.Getpid(), nil }, + Namespace: "foobar", + ReportErrors: true, // No errors expected, just to see if none are reported. + })); err != nil { t.Fatal(err) } @@ -38,21 +58,46 @@ func TestProcessCollector(t *testing.T) { } for _, re := range []*regexp.Regexp{ - regexp.MustCompile("process_cpu_seconds_total [0-9]"), - regexp.MustCompile("process_max_fds [1-9]"), - regexp.MustCompile("process_open_fds [1-9]"), - regexp.MustCompile("process_virtual_memory_bytes [1-9]"), - regexp.MustCompile("process_resident_memory_bytes [1-9]"), - regexp.MustCompile("process_start_time_seconds [0-9.]{10,}"), - regexp.MustCompile("foobar_process_cpu_seconds_total [0-9]"), - regexp.MustCompile("foobar_process_max_fds [1-9]"), - regexp.MustCompile("foobar_process_open_fds [1-9]"), - regexp.MustCompile("foobar_process_virtual_memory_bytes [1-9]"), - regexp.MustCompile("foobar_process_resident_memory_bytes [1-9]"), - regexp.MustCompile("foobar_process_start_time_seconds [0-9.]{10,}"), + regexp.MustCompile("\nprocess_cpu_seconds_total [0-9]"), + regexp.MustCompile("\nprocess_max_fds [1-9]"), + regexp.MustCompile("\nprocess_open_fds [1-9]"), + regexp.MustCompile("\nprocess_virtual_memory_max_bytes (-1|[1-9])"), + regexp.MustCompile("\nprocess_virtual_memory_bytes [1-9]"), + regexp.MustCompile("\nprocess_resident_memory_bytes [1-9]"), + regexp.MustCompile("\nprocess_start_time_seconds [0-9.]{10,}"), + regexp.MustCompile("\nfoobar_process_cpu_seconds_total [0-9]"), + regexp.MustCompile("\nfoobar_process_max_fds [1-9]"), + regexp.MustCompile("\nfoobar_process_open_fds [1-9]"), + regexp.MustCompile("\nfoobar_process_virtual_memory_max_bytes (-1|[1-9])"), + regexp.MustCompile("\nfoobar_process_virtual_memory_bytes [1-9]"), + regexp.MustCompile("\nfoobar_process_resident_memory_bytes [1-9]"), + regexp.MustCompile("\nfoobar_process_start_time_seconds [0-9.]{10,}"), } { if !re.Match(buf.Bytes()) { t.Errorf("want body to match %s\n%s", re, buf.String()) } } + + brokenProcessCollector := NewProcessCollector(ProcessCollectorOpts{ + PidFn: func() (int, error) { return 0, errors.New("boo") }, + ReportErrors: true, + }) + + ch := make(chan Metric) + go func() { + brokenProcessCollector.Collect(ch) + close(ch) + }() + n := 0 + for m := range ch { + n++ + pb := &dto.Metric{} + err := m.Write(pb) + if err == nil { + t.Error("metric collected from broken process collector is unexpectedly valid") + } + } + if n != 1 { + t.Errorf("%d metrics collected, want 1", n) + } } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promauto/auto.go b/vendor/github.com/prometheus/client_golang/prometheus/promauto/auto.go new file mode 100644 index 0000000000..a00ba1eb83 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/promauto/auto.go @@ -0,0 +1,223 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package promauto provides constructors for the usual Prometheus metrics that +// return them already registered with the global registry +// (prometheus.DefaultRegisterer). This allows very compact code, avoiding any +// references to the registry altogether, but all the constructors in this +// package will panic if the registration fails. +// +// The following example is a complete program to create a histogram of normally +// distributed random numbers from the math/rand package: +// +// package main +// +// import ( +// "math/rand" +// "net/http" +// +// "github.com/prometheus/client_golang/prometheus" +// "github.com/prometheus/client_golang/prometheus/promauto" +// "github.com/prometheus/client_golang/prometheus/promhttp" +// ) +// +// var histogram = promauto.NewHistogram(prometheus.HistogramOpts{ +// Name: "random_numbers", +// Help: "A histogram of normally distributed random numbers.", +// Buckets: prometheus.LinearBuckets(-3, .1, 61), +// }) +// +// func Random() { +// for { +// histogram.Observe(rand.NormFloat64()) +// } +// } +// +// func main() { +// go Random() +// http.Handle("/metrics", promhttp.Handler()) +// http.ListenAndServe(":1971", nil) +// } +// +// Prometheus's version of a minimal hello-world program: +// +// package main +// +// import ( +// "fmt" +// "net/http" +// +// "github.com/prometheus/client_golang/prometheus" +// "github.com/prometheus/client_golang/prometheus/promauto" +// "github.com/prometheus/client_golang/prometheus/promhttp" +// ) +// +// func main() { +// http.Handle("/", promhttp.InstrumentHandlerCounter( +// promauto.NewCounterVec( +// prometheus.CounterOpts{ +// Name: "hello_requests_total", +// Help: "Total number of hello-world requests by HTTP code.", +// }, +// []string{"code"}, +// ), +// http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// fmt.Fprint(w, "Hello, world!") +// }), +// )) +// http.Handle("/metrics", promhttp.Handler()) +// http.ListenAndServe(":1971", nil) +// } +// +// This appears very handy. So why are these constructors locked away in a +// separate package? There are two caveats: +// +// First, in more complex programs, global state is often quite problematic. +// That's the reason why the metrics constructors in the prometheus package do +// not interact with the global prometheus.DefaultRegisterer on their own. You +// are free to use the Register or MustRegister functions to register them with +// the global prometheus.DefaultRegisterer, but you could as well choose a local +// Registerer (usually created with prometheus.NewRegistry, but there are other +// scenarios, e.g. testing). +// +// The second issue is that registration may fail, e.g. if a metric inconsistent +// with the newly to be registered one is already registered. But how to signal +// and handle a panic in the automatic registration with the default registry? +// The only way is panicking. While panicking on invalid input provided by the +// programmer is certainly fine, things are a bit more subtle in this case: You +// might just add another package to the program, and that package (in its init +// function) happens to register a metric with the same name as your code. Now, +// all of a sudden, either your code or the code of the newly imported package +// panics, depending on initialization order, without any opportunity to handle +// the case gracefully. Even worse is a scenario where registration happens +// later during the runtime (e.g. upon loading some kind of plugin), where the +// panic could be triggered long after the code has been deployed to +// production. A possibility to panic should be explicitly called out by the +// Must… idiom, cf. prometheus.MustRegister. But adding a separate set of +// constructors in the prometheus package called MustRegisterNewCounterVec or +// similar would be quite unwieldy. Adding an extra MustRegister method to each +// metric, returning the registered metric, would result in nice code for those +// using the method, but would pollute every single metric interface for +// everybody avoiding the global registry. +// +// To address both issues, the problematic auto-registering and possibly +// panicking constructors are all in this package with a clear warning +// ahead. And whoever cares about avoiding global state and possibly panicking +// function calls can simply ignore the existence of the promauto package +// altogether. +// +// A final note: There is a similar case in the net/http package of the standard +// library. It has DefaultServeMux as a global instance of ServeMux, and the +// Handle function acts on it, panicking if a handler for the same pattern has +// already been registered. However, one might argue that the whole HTTP routing +// is usually set up closely together in the same package or file, while +// Prometheus metrics tend to be spread widely over the codebase, increasing the +// chance of surprising registration failures. Furthermore, the use of global +// state in net/http has been criticized widely, and some avoid it altogether. +package promauto + +import "github.com/prometheus/client_golang/prometheus" + +// NewCounter works like the function of the same name in the prometheus package +// but it automatically registers the Counter with the +// prometheus.DefaultRegisterer. If the registration fails, NewCounter panics. +func NewCounter(opts prometheus.CounterOpts) prometheus.Counter { + c := prometheus.NewCounter(opts) + prometheus.MustRegister(c) + return c +} + +// NewCounterVec works like the function of the same name in the prometheus +// package but it automatically registers the CounterVec with the +// prometheus.DefaultRegisterer. If the registration fails, NewCounterVec +// panics. +func NewCounterVec(opts prometheus.CounterOpts, labelNames []string) *prometheus.CounterVec { + c := prometheus.NewCounterVec(opts, labelNames) + prometheus.MustRegister(c) + return c +} + +// NewCounterFunc works like the function of the same name in the prometheus +// package but it automatically registers the CounterFunc with the +// prometheus.DefaultRegisterer. If the registration fails, NewCounterFunc +// panics. +func NewCounterFunc(opts prometheus.CounterOpts, function func() float64) prometheus.CounterFunc { + g := prometheus.NewCounterFunc(opts, function) + prometheus.MustRegister(g) + return g +} + +// NewGauge works like the function of the same name in the prometheus package +// but it automatically registers the Gauge with the +// prometheus.DefaultRegisterer. If the registration fails, NewGauge panics. +func NewGauge(opts prometheus.GaugeOpts) prometheus.Gauge { + g := prometheus.NewGauge(opts) + prometheus.MustRegister(g) + return g +} + +// NewGaugeVec works like the function of the same name in the prometheus +// package but it automatically registers the GaugeVec with the +// prometheus.DefaultRegisterer. If the registration fails, NewGaugeVec panics. +func NewGaugeVec(opts prometheus.GaugeOpts, labelNames []string) *prometheus.GaugeVec { + g := prometheus.NewGaugeVec(opts, labelNames) + prometheus.MustRegister(g) + return g +} + +// NewGaugeFunc works like the function of the same name in the prometheus +// package but it automatically registers the GaugeFunc with the +// prometheus.DefaultRegisterer. If the registration fails, NewGaugeFunc panics. +func NewGaugeFunc(opts prometheus.GaugeOpts, function func() float64) prometheus.GaugeFunc { + g := prometheus.NewGaugeFunc(opts, function) + prometheus.MustRegister(g) + return g +} + +// NewSummary works like the function of the same name in the prometheus package +// but it automatically registers the Summary with the +// prometheus.DefaultRegisterer. If the registration fails, NewSummary panics. +func NewSummary(opts prometheus.SummaryOpts) prometheus.Summary { + s := prometheus.NewSummary(opts) + prometheus.MustRegister(s) + return s +} + +// NewSummaryVec works like the function of the same name in the prometheus +// package but it automatically registers the SummaryVec with the +// prometheus.DefaultRegisterer. If the registration fails, NewSummaryVec +// panics. +func NewSummaryVec(opts prometheus.SummaryOpts, labelNames []string) *prometheus.SummaryVec { + s := prometheus.NewSummaryVec(opts, labelNames) + prometheus.MustRegister(s) + return s +} + +// NewHistogram works like the function of the same name in the prometheus +// package but it automatically registers the Histogram with the +// prometheus.DefaultRegisterer. If the registration fails, NewHistogram panics. +func NewHistogram(opts prometheus.HistogramOpts) prometheus.Histogram { + h := prometheus.NewHistogram(opts) + prometheus.MustRegister(h) + return h +} + +// NewHistogramVec works like the function of the same name in the prometheus +// package but it automatically registers the HistogramVec with the +// prometheus.DefaultRegisterer. If the registration fails, NewHistogramVec +// panics. +func NewHistogramVec(opts prometheus.HistogramOpts, labelNames []string) *prometheus.HistogramVec { + h := prometheus.NewHistogramVec(opts, labelNames) + prometheus.MustRegister(h) + return h +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/delegator.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/delegator.go new file mode 100644 index 0000000000..67b56d37cf --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/delegator.go @@ -0,0 +1,199 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package promhttp + +import ( + "bufio" + "io" + "net" + "net/http" +) + +const ( + closeNotifier = 1 << iota + flusher + hijacker + readerFrom + pusher +) + +type delegator interface { + http.ResponseWriter + + Status() int + Written() int64 +} + +type responseWriterDelegator struct { + http.ResponseWriter + + handler, method string + status int + written int64 + wroteHeader bool + observeWriteHeader func(int) +} + +func (r *responseWriterDelegator) Status() int { + return r.status +} + +func (r *responseWriterDelegator) Written() int64 { + return r.written +} + +func (r *responseWriterDelegator) WriteHeader(code int) { + r.status = code + r.wroteHeader = true + r.ResponseWriter.WriteHeader(code) + if r.observeWriteHeader != nil { + r.observeWriteHeader(code) + } +} + +func (r *responseWriterDelegator) Write(b []byte) (int, error) { + if !r.wroteHeader { + r.WriteHeader(http.StatusOK) + } + n, err := r.ResponseWriter.Write(b) + r.written += int64(n) + return n, err +} + +type closeNotifierDelegator struct{ *responseWriterDelegator } +type flusherDelegator struct{ *responseWriterDelegator } +type hijackerDelegator struct{ *responseWriterDelegator } +type readerFromDelegator struct{ *responseWriterDelegator } + +func (d closeNotifierDelegator) CloseNotify() <-chan bool { + return d.ResponseWriter.(http.CloseNotifier).CloseNotify() +} +func (d flusherDelegator) Flush() { + d.ResponseWriter.(http.Flusher).Flush() +} +func (d hijackerDelegator) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return d.ResponseWriter.(http.Hijacker).Hijack() +} +func (d readerFromDelegator) ReadFrom(re io.Reader) (int64, error) { + if !d.wroteHeader { + d.WriteHeader(http.StatusOK) + } + n, err := d.ResponseWriter.(io.ReaderFrom).ReadFrom(re) + d.written += n + return n, err +} + +var pickDelegator = make([]func(*responseWriterDelegator) delegator, 32) + +func init() { + // TODO(beorn7): Code generation would help here. + pickDelegator[0] = func(d *responseWriterDelegator) delegator { // 0 + return d + } + pickDelegator[closeNotifier] = func(d *responseWriterDelegator) delegator { // 1 + return closeNotifierDelegator{d} + } + pickDelegator[flusher] = func(d *responseWriterDelegator) delegator { // 2 + return flusherDelegator{d} + } + pickDelegator[flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 3 + return struct { + *responseWriterDelegator + http.Flusher + http.CloseNotifier + }{d, flusherDelegator{d}, closeNotifierDelegator{d}} + } + pickDelegator[hijacker] = func(d *responseWriterDelegator) delegator { // 4 + return hijackerDelegator{d} + } + pickDelegator[hijacker+closeNotifier] = func(d *responseWriterDelegator) delegator { // 5 + return struct { + *responseWriterDelegator + http.Hijacker + http.CloseNotifier + }{d, hijackerDelegator{d}, closeNotifierDelegator{d}} + } + pickDelegator[hijacker+flusher] = func(d *responseWriterDelegator) delegator { // 6 + return struct { + *responseWriterDelegator + http.Hijacker + http.Flusher + }{d, hijackerDelegator{d}, flusherDelegator{d}} + } + pickDelegator[hijacker+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 7 + return struct { + *responseWriterDelegator + http.Hijacker + http.Flusher + http.CloseNotifier + }{d, hijackerDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}} + } + pickDelegator[readerFrom] = func(d *responseWriterDelegator) delegator { // 8 + return readerFromDelegator{d} + } + pickDelegator[readerFrom+closeNotifier] = func(d *responseWriterDelegator) delegator { // 9 + return struct { + *responseWriterDelegator + io.ReaderFrom + http.CloseNotifier + }{d, readerFromDelegator{d}, closeNotifierDelegator{d}} + } + pickDelegator[readerFrom+flusher] = func(d *responseWriterDelegator) delegator { // 10 + return struct { + *responseWriterDelegator + io.ReaderFrom + http.Flusher + }{d, readerFromDelegator{d}, flusherDelegator{d}} + } + pickDelegator[readerFrom+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 11 + return struct { + *responseWriterDelegator + io.ReaderFrom + http.Flusher + http.CloseNotifier + }{d, readerFromDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}} + } + pickDelegator[readerFrom+hijacker] = func(d *responseWriterDelegator) delegator { // 12 + return struct { + *responseWriterDelegator + io.ReaderFrom + http.Hijacker + }{d, readerFromDelegator{d}, hijackerDelegator{d}} + } + pickDelegator[readerFrom+hijacker+closeNotifier] = func(d *responseWriterDelegator) delegator { // 13 + return struct { + *responseWriterDelegator + io.ReaderFrom + http.Hijacker + http.CloseNotifier + }{d, readerFromDelegator{d}, hijackerDelegator{d}, closeNotifierDelegator{d}} + } + pickDelegator[readerFrom+hijacker+flusher] = func(d *responseWriterDelegator) delegator { // 14 + return struct { + *responseWriterDelegator + io.ReaderFrom + http.Hijacker + http.Flusher + }{d, readerFromDelegator{d}, hijackerDelegator{d}, flusherDelegator{d}} + } + pickDelegator[readerFrom+hijacker+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 15 + return struct { + *responseWriterDelegator + io.ReaderFrom + http.Hijacker + http.Flusher + http.CloseNotifier + }{d, readerFromDelegator{d}, hijackerDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}} + } +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/delegator_1_8.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/delegator_1_8.go new file mode 100644 index 0000000000..31a7069569 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/delegator_1_8.go @@ -0,0 +1,181 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build go1.8 + +package promhttp + +import ( + "io" + "net/http" +) + +type pusherDelegator struct{ *responseWriterDelegator } + +func (d pusherDelegator) Push(target string, opts *http.PushOptions) error { + return d.ResponseWriter.(http.Pusher).Push(target, opts) +} + +func init() { + pickDelegator[pusher] = func(d *responseWriterDelegator) delegator { // 16 + return pusherDelegator{d} + } + pickDelegator[pusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 17 + return struct { + *responseWriterDelegator + http.Pusher + http.CloseNotifier + }{d, pusherDelegator{d}, closeNotifierDelegator{d}} + } + pickDelegator[pusher+flusher] = func(d *responseWriterDelegator) delegator { // 18 + return struct { + *responseWriterDelegator + http.Pusher + http.Flusher + }{d, pusherDelegator{d}, flusherDelegator{d}} + } + pickDelegator[pusher+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 19 + return struct { + *responseWriterDelegator + http.Pusher + http.Flusher + http.CloseNotifier + }{d, pusherDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}} + } + pickDelegator[pusher+hijacker] = func(d *responseWriterDelegator) delegator { // 20 + return struct { + *responseWriterDelegator + http.Pusher + http.Hijacker + }{d, pusherDelegator{d}, hijackerDelegator{d}} + } + pickDelegator[pusher+hijacker+closeNotifier] = func(d *responseWriterDelegator) delegator { // 21 + return struct { + *responseWriterDelegator + http.Pusher + http.Hijacker + http.CloseNotifier + }{d, pusherDelegator{d}, hijackerDelegator{d}, closeNotifierDelegator{d}} + } + pickDelegator[pusher+hijacker+flusher] = func(d *responseWriterDelegator) delegator { // 22 + return struct { + *responseWriterDelegator + http.Pusher + http.Hijacker + http.Flusher + }{d, pusherDelegator{d}, hijackerDelegator{d}, flusherDelegator{d}} + } + pickDelegator[pusher+hijacker+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { //23 + return struct { + *responseWriterDelegator + http.Pusher + http.Hijacker + http.Flusher + http.CloseNotifier + }{d, pusherDelegator{d}, hijackerDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}} + } + pickDelegator[pusher+readerFrom] = func(d *responseWriterDelegator) delegator { // 24 + return struct { + *responseWriterDelegator + http.Pusher + io.ReaderFrom + }{d, pusherDelegator{d}, readerFromDelegator{d}} + } + pickDelegator[pusher+readerFrom+closeNotifier] = func(d *responseWriterDelegator) delegator { // 25 + return struct { + *responseWriterDelegator + http.Pusher + io.ReaderFrom + http.CloseNotifier + }{d, pusherDelegator{d}, readerFromDelegator{d}, closeNotifierDelegator{d}} + } + pickDelegator[pusher+readerFrom+flusher] = func(d *responseWriterDelegator) delegator { // 26 + return struct { + *responseWriterDelegator + http.Pusher + io.ReaderFrom + http.Flusher + }{d, pusherDelegator{d}, readerFromDelegator{d}, flusherDelegator{d}} + } + pickDelegator[pusher+readerFrom+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 27 + return struct { + *responseWriterDelegator + http.Pusher + io.ReaderFrom + http.Flusher + http.CloseNotifier + }{d, pusherDelegator{d}, readerFromDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}} + } + pickDelegator[pusher+readerFrom+hijacker] = func(d *responseWriterDelegator) delegator { // 28 + return struct { + *responseWriterDelegator + http.Pusher + io.ReaderFrom + http.Hijacker + }{d, pusherDelegator{d}, readerFromDelegator{d}, hijackerDelegator{d}} + } + pickDelegator[pusher+readerFrom+hijacker+closeNotifier] = func(d *responseWriterDelegator) delegator { // 29 + return struct { + *responseWriterDelegator + http.Pusher + io.ReaderFrom + http.Hijacker + http.CloseNotifier + }{d, pusherDelegator{d}, readerFromDelegator{d}, hijackerDelegator{d}, closeNotifierDelegator{d}} + } + pickDelegator[pusher+readerFrom+hijacker+flusher] = func(d *responseWriterDelegator) delegator { // 30 + return struct { + *responseWriterDelegator + http.Pusher + io.ReaderFrom + http.Hijacker + http.Flusher + }{d, pusherDelegator{d}, readerFromDelegator{d}, hijackerDelegator{d}, flusherDelegator{d}} + } + pickDelegator[pusher+readerFrom+hijacker+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 31 + return struct { + *responseWriterDelegator + http.Pusher + io.ReaderFrom + http.Hijacker + http.Flusher + http.CloseNotifier + }{d, pusherDelegator{d}, readerFromDelegator{d}, hijackerDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}} + } +} + +func newDelegator(w http.ResponseWriter, observeWriteHeaderFunc func(int)) delegator { + d := &responseWriterDelegator{ + ResponseWriter: w, + observeWriteHeader: observeWriteHeaderFunc, + } + + id := 0 + if _, ok := w.(http.CloseNotifier); ok { + id += closeNotifier + } + if _, ok := w.(http.Flusher); ok { + id += flusher + } + if _, ok := w.(http.Hijacker); ok { + id += hijacker + } + if _, ok := w.(io.ReaderFrom); ok { + id += readerFrom + } + if _, ok := w.(http.Pusher); ok { + id += pusher + } + + return pickDelegator[id](d) +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/delegator_pre_1_8.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/delegator_pre_1_8.go new file mode 100644 index 0000000000..8bb9b8b68f --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/delegator_pre_1_8.go @@ -0,0 +1,44 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !go1.8 + +package promhttp + +import ( + "io" + "net/http" +) + +func newDelegator(w http.ResponseWriter, observeWriteHeaderFunc func(int)) delegator { + d := &responseWriterDelegator{ + ResponseWriter: w, + observeWriteHeader: observeWriteHeaderFunc, + } + + id := 0 + if _, ok := w.(http.CloseNotifier); ok { + id += closeNotifier + } + if _, ok := w.(http.Flusher); ok { + id += flusher + } + if _, ok := w.(http.Hijacker); ok { + id += hijacker + } + if _, ok := w.(io.ReaderFrom); ok { + id += readerFrom + } + + return pickDelegator[id](d) +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go index b6dd5a266c..668eb6b3c9 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go @@ -11,31 +11,34 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Copyright (c) 2013, The Prometheus Authors -// All rights reserved. +// Package promhttp provides tooling around HTTP servers and clients. // -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -// Package promhttp contains functions to create http.Handler instances to -// expose Prometheus metrics via HTTP. In later versions of this package, it -// will also contain tooling to instrument instances of http.Handler and -// http.RoundTripper. +// First, the package allows the creation of http.Handler instances to expose +// Prometheus metrics via HTTP. promhttp.Handler acts on the +// prometheus.DefaultGatherer. With HandlerFor, you can create a handler for a +// custom registry or anything that implements the Gatherer interface. It also +// allows the creation of handlers that act differently on errors or allow to +// log errors. +// +// Second, the package provides tooling to instrument instances of http.Handler +// via middleware. Middleware wrappers follow the naming scheme +// InstrumentHandlerX, where X describes the intended use of the middleware. +// See each function's doc comment for specific details. // -// promhttp.Handler acts on the prometheus.DefaultGatherer. With HandlerFor, -// you can create a handler for a custom registry or anything that implements -// the Gatherer interface. It also allows to create handlers that act -// differently on errors or allow to log errors. +// Finally, the package allows for an http.RoundTripper to be instrumented via +// middleware. Middleware wrappers follow the naming scheme +// InstrumentRoundTripperX, where X describes the intended use of the +// middleware. See each function's doc comment for specific details. package promhttp import ( - "bytes" "compress/gzip" "fmt" "io" "net/http" "strings" "sync" + "time" "github.com/prometheus/common/expfmt" @@ -49,36 +52,56 @@ const ( acceptEncodingHeader = "Accept-Encoding" ) -var bufPool sync.Pool - -func getBuf() *bytes.Buffer { - buf := bufPool.Get() - if buf == nil { - return &bytes.Buffer{} - } - return buf.(*bytes.Buffer) +var gzipPool = sync.Pool{ + New: func() interface{} { + return gzip.NewWriter(nil) + }, } -func giveBuf(buf *bytes.Buffer) { - buf.Reset() - bufPool.Put(buf) -} - -// Handler returns an HTTP handler for the prometheus.DefaultGatherer. The -// Handler uses the default HandlerOpts, i.e. report the first error as an HTTP -// error, no error logging, and compression if requested by the client. +// Handler returns an http.Handler for the prometheus.DefaultGatherer, using +// default HandlerOpts, i.e. it reports the first error as an HTTP error, it has +// no error logging, and it applies compression if requested by the client. +// +// The returned http.Handler is already instrumented using the +// InstrumentMetricHandler function and the prometheus.DefaultRegisterer. If you +// create multiple http.Handlers by separate calls of the Handler function, the +// metrics used for instrumentation will be shared between them, providing +// global scrape counts. // -// If you want to create a Handler for the DefaultGatherer with different -// HandlerOpts, create it with HandlerFor with prometheus.DefaultGatherer and -// your desired HandlerOpts. +// This function is meant to cover the bulk of basic use cases. If you are doing +// anything that requires more customization (including using a non-default +// Gatherer, different instrumentation, and non-default HandlerOpts), use the +// HandlerFor function. See there for details. func Handler() http.Handler { - return HandlerFor(prometheus.DefaultGatherer, HandlerOpts{}) + return InstrumentMetricHandler( + prometheus.DefaultRegisterer, HandlerFor(prometheus.DefaultGatherer, HandlerOpts{}), + ) } -// HandlerFor returns an http.Handler for the provided Gatherer. The behavior -// of the Handler is defined by the provided HandlerOpts. +// HandlerFor returns an uninstrumented http.Handler for the provided +// Gatherer. The behavior of the Handler is defined by the provided +// HandlerOpts. Thus, HandlerFor is useful to create http.Handlers for custom +// Gatherers, with non-default HandlerOpts, and/or with custom (or no) +// instrumentation. Use the InstrumentMetricHandler function to apply the same +// kind of instrumentation as it is used by the Handler function. func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + var inFlightSem chan struct{} + if opts.MaxRequestsInFlight > 0 { + inFlightSem = make(chan struct{}, opts.MaxRequestsInFlight) + } + + h := http.HandlerFunc(func(rsp http.ResponseWriter, req *http.Request) { + if inFlightSem != nil { + select { + case inFlightSem <- struct{}{}: // All good, carry on. + defer func() { <-inFlightSem }() + default: + http.Error(rsp, fmt.Sprintf( + "Limit of concurrent requests reached (%d), try again later.", opts.MaxRequestsInFlight, + ), http.StatusServiceUnavailable) + return + } + } mfs, err := reg.Gather() if err != nil { if opts.ErrorLog != nil { @@ -89,26 +112,40 @@ func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler { panic(err) case ContinueOnError: if len(mfs) == 0 { - http.Error(w, "No metrics gathered, last error:\n\n"+err.Error(), http.StatusInternalServerError) + // Still report the error if no metrics have been gathered. + httpError(rsp, err) return } case HTTPErrorOnError: - http.Error(w, "An error has occurred during metrics gathering:\n\n"+err.Error(), http.StatusInternalServerError) + httpError(rsp, err) return } } contentType := expfmt.Negotiate(req.Header) - buf := getBuf() - defer giveBuf(buf) - writer, encoding := decorateWriter(req, buf, opts.DisableCompression) - enc := expfmt.NewEncoder(writer, contentType) + header := rsp.Header() + header.Set(contentTypeHeader, string(contentType)) + + w := io.Writer(rsp) + if !opts.DisableCompression && gzipAccepted(req.Header) { + header.Set(contentEncodingHeader, "gzip") + gz := gzipPool.Get().(*gzip.Writer) + defer gzipPool.Put(gz) + + gz.Reset(w) + defer gz.Close() + + w = gz + } + + enc := expfmt.NewEncoder(w, contentType) + var lastErr error for _, mf := range mfs { if err := enc.Encode(mf); err != nil { lastErr = err if opts.ErrorLog != nil { - opts.ErrorLog.Println("error encoding metric family:", err) + opts.ErrorLog.Println("error encoding and sending metric family:", err) } switch opts.ErrorHandling { case PanicOnError: @@ -116,27 +153,75 @@ func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler { case ContinueOnError: // Handled later. case HTTPErrorOnError: - http.Error(w, "An error has occurred during metrics encoding:\n\n"+err.Error(), http.StatusInternalServerError) + httpError(rsp, err) return } } } - if closer, ok := writer.(io.Closer); ok { - closer.Close() - } - if lastErr != nil && buf.Len() == 0 { - http.Error(w, "No metrics encoded, last error:\n\n"+err.Error(), http.StatusInternalServerError) - return + + if lastErr != nil { + httpError(rsp, lastErr) } - header := w.Header() - header.Set(contentTypeHeader, string(contentType)) - header.Set(contentLengthHeader, fmt.Sprint(buf.Len())) - if encoding != "" { - header.Set(contentEncodingHeader, encoding) + }) + + if opts.Timeout <= 0 { + return h + } + return http.TimeoutHandler(h, opts.Timeout, fmt.Sprintf( + "Exceeded configured timeout of %v.\n", + opts.Timeout, + )) +} + +// InstrumentMetricHandler is usually used with an http.Handler returned by the +// HandlerFor function. It instruments the provided http.Handler with two +// metrics: A counter vector "promhttp_metric_handler_requests_total" to count +// scrapes partitioned by HTTP status code, and a gauge +// "promhttp_metric_handler_requests_in_flight" to track the number of +// simultaneous scrapes. This function idempotently registers collectors for +// both metrics with the provided Registerer. It panics if the registration +// fails. The provided metrics are useful to see how many scrapes hit the +// monitored target (which could be from different Prometheus servers or other +// scrapers), and how often they overlap (which would result in more than one +// scrape in flight at the same time). Note that the scrapes-in-flight gauge +// will contain the scrape by which it is exposed, while the scrape counter will +// only get incremented after the scrape is complete (as only then the status +// code is known). For tracking scrape durations, use the +// "scrape_duration_seconds" gauge created by the Prometheus server upon each +// scrape. +func InstrumentMetricHandler(reg prometheus.Registerer, handler http.Handler) http.Handler { + cnt := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "promhttp_metric_handler_requests_total", + Help: "Total number of scrapes by HTTP status code.", + }, + []string{"code"}, + ) + // Initialize the most likely HTTP status codes. + cnt.WithLabelValues("200") + cnt.WithLabelValues("500") + cnt.WithLabelValues("503") + if err := reg.Register(cnt); err != nil { + if are, ok := err.(prometheus.AlreadyRegisteredError); ok { + cnt = are.ExistingCollector.(*prometheus.CounterVec) + } else { + panic(err) } - w.Write(buf.Bytes()) - // TODO(beorn7): Consider streaming serving of metrics. + } + + gge := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "promhttp_metric_handler_requests_in_flight", + Help: "Current number of scrapes being served.", }) + if err := reg.Register(gge); err != nil { + if are, ok := err.(prometheus.AlreadyRegisteredError); ok { + gge = are.ExistingCollector.(prometheus.Gauge) + } else { + panic(err) + } + } + + return InstrumentHandlerCounter(cnt, InstrumentHandlerInFlight(gge, handler)) } // HandlerErrorHandling defines how a Handler serving metrics will handle @@ -180,22 +265,47 @@ type HandlerOpts struct { // If DisableCompression is true, the handler will never compress the // response, even if requested by the client. DisableCompression bool + // The number of concurrent HTTP requests is limited to + // MaxRequestsInFlight. Additional requests are responded to with 503 + // Service Unavailable and a suitable message in the body. If + // MaxRequestsInFlight is 0 or negative, no limit is applied. + MaxRequestsInFlight int + // If handling a request takes longer than Timeout, it is responded to + // with 503 ServiceUnavailable and a suitable Message. No timeout is + // applied if Timeout is 0 or negative. Note that with the current + // implementation, reaching the timeout simply ends the HTTP requests as + // described above (and even that only if sending of the body hasn't + // started yet), while the bulk work of gathering all the metrics keeps + // running in the background (with the eventual result to be thrown + // away). Until the implementation is improved, it is recommended to + // implement a separate timeout in potentially slow Collectors. + Timeout time.Duration } -// decorateWriter wraps a writer to handle gzip compression if requested. It -// returns the decorated writer and the appropriate "Content-Encoding" header -// (which is empty if no compression is enabled). -func decorateWriter(request *http.Request, writer io.Writer, compressionDisabled bool) (io.Writer, string) { - if compressionDisabled { - return writer, "" - } - header := request.Header.Get(acceptEncodingHeader) - parts := strings.Split(header, ",") +// gzipAccepted returns whether the client will accept gzip-encoded content. +func gzipAccepted(header http.Header) bool { + a := header.Get(acceptEncodingHeader) + parts := strings.Split(a, ",") for _, part := range parts { - part := strings.TrimSpace(part) + part = strings.TrimSpace(part) if part == "gzip" || strings.HasPrefix(part, "gzip;") { - return gzip.NewWriter(writer), "gzip" + return true } } - return writer, "" + return false +} + +// httpError removes any content-encoding header and then calls http.Error with +// the provided error and http.StatusInternalServerErrer. Error contents is +// supposed to be uncompressed plain text. However, same as with a plain +// http.Error, any header settings will be void if the header has already been +// sent. The error message will still be written to the writer, but it will +// probably be of limited use. +func httpError(rsp http.ResponseWriter, err error) { + rsp.Header().Del(contentEncodingHeader) + http.Error( + rsp, + "An error has occurred while serving metrics:\n\n"+err.Error(), + http.StatusInternalServerError, + ) } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http_test.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http_test.go index d4a7d4a7b5..6e23e6c6dc 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http_test.go @@ -11,12 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Copyright (c) 2013, The Prometheus Authors -// All rights reserved. -// -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - package promhttp import ( @@ -25,7 +19,9 @@ import ( "log" "net/http" "net/http/httptest" + "strings" "testing" + "time" "github.com/prometheus/client_golang/prometheus" ) @@ -43,6 +39,23 @@ func (e errorCollector) Collect(ch chan<- prometheus.Metric) { ) } +type blockingCollector struct { + CollectStarted, Block chan struct{} +} + +func (b blockingCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- prometheus.NewDesc("dummy_desc", "not helpful", nil, nil) +} + +func (b blockingCollector) Collect(ch chan<- prometheus.Metric) { + select { + case b.CollectStarted <- struct{}{}: + default: + } + // Collects nothing, just waits for a channel receive. + <-b.Block +} + func TestHandlerErrorHandling(t *testing.T) { // Create a registry that collects a MetricFamily with two elements, @@ -90,7 +103,7 @@ func TestHandlerErrorHandling(t *testing.T) { }) wantMsg := `error gathering metrics: error collecting metric Desc{fqName: "invalid_metric", help: "not helpful", constLabels: {}, variableLabels: []}: collect error ` - wantErrorBody := `An error has occurred during metrics gathering: + wantErrorBody := `An error has occurred while serving metrics: error collecting metric Desc{fqName: "invalid_metric", help: "not helpful", constLabels: {}, variableLabels: []}: collect error ` @@ -108,7 +121,7 @@ the_count 0 t.Errorf("got HTTP status code %d, want %d", got, want) } if got := logBuf.String(); got != wantMsg { - t.Errorf("got log message:\n%s\nwant log mesage:\n%s\n", got, wantMsg) + t.Errorf("got log message:\n%s\nwant log message:\n%s\n", got, wantMsg) } if got := writer.Body.String(); got != wantErrorBody { t.Errorf("got body:\n%s\nwant body:\n%s\n", got, wantErrorBody) @@ -135,3 +148,103 @@ the_count 0 }() panicHandler.ServeHTTP(writer, request) } + +func TestInstrumentMetricHandler(t *testing.T) { + reg := prometheus.NewRegistry() + handler := InstrumentMetricHandler(reg, HandlerFor(reg, HandlerOpts{})) + // Do it again to test idempotency. + InstrumentMetricHandler(reg, HandlerFor(reg, HandlerOpts{})) + writer := httptest.NewRecorder() + request, _ := http.NewRequest("GET", "/", nil) + request.Header.Add("Accept", "test/plain") + + handler.ServeHTTP(writer, request) + if got, want := writer.Code, http.StatusOK; got != want { + t.Errorf("got HTTP status code %d, want %d", got, want) + } + + want := "promhttp_metric_handler_requests_in_flight 1\n" + if got := writer.Body.String(); !strings.Contains(got, want) { + t.Errorf("got body %q, does not contain %q", got, want) + } + want = "promhttp_metric_handler_requests_total{code=\"200\"} 0\n" + if got := writer.Body.String(); !strings.Contains(got, want) { + t.Errorf("got body %q, does not contain %q", got, want) + } + + writer.Body.Reset() + handler.ServeHTTP(writer, request) + if got, want := writer.Code, http.StatusOK; got != want { + t.Errorf("got HTTP status code %d, want %d", got, want) + } + + want = "promhttp_metric_handler_requests_in_flight 1\n" + if got := writer.Body.String(); !strings.Contains(got, want) { + t.Errorf("got body %q, does not contain %q", got, want) + } + want = "promhttp_metric_handler_requests_total{code=\"200\"} 1\n" + if got := writer.Body.String(); !strings.Contains(got, want) { + t.Errorf("got body %q, does not contain %q", got, want) + } +} + +func TestHandlerMaxRequestsInFlight(t *testing.T) { + reg := prometheus.NewRegistry() + handler := HandlerFor(reg, HandlerOpts{MaxRequestsInFlight: 1}) + w1 := httptest.NewRecorder() + w2 := httptest.NewRecorder() + w3 := httptest.NewRecorder() + request, _ := http.NewRequest("GET", "/", nil) + request.Header.Add("Accept", "test/plain") + + c := blockingCollector{Block: make(chan struct{}), CollectStarted: make(chan struct{}, 1)} + reg.MustRegister(c) + + rq1Done := make(chan struct{}) + go func() { + handler.ServeHTTP(w1, request) + close(rq1Done) + }() + <-c.CollectStarted + + handler.ServeHTTP(w2, request) + + if got, want := w2.Code, http.StatusServiceUnavailable; got != want { + t.Errorf("got HTTP status code %d, want %d", got, want) + } + if got, want := w2.Body.String(), "Limit of concurrent requests reached (1), try again later.\n"; got != want { + t.Errorf("got body %q, want %q", got, want) + } + + close(c.Block) + <-rq1Done + + handler.ServeHTTP(w3, request) + + if got, want := w3.Code, http.StatusOK; got != want { + t.Errorf("got HTTP status code %d, want %d", got, want) + } +} + +func TestHandlerTimeout(t *testing.T) { + reg := prometheus.NewRegistry() + handler := HandlerFor(reg, HandlerOpts{Timeout: time.Millisecond}) + w := httptest.NewRecorder() + + request, _ := http.NewRequest("GET", "/", nil) + request.Header.Add("Accept", "test/plain") + + c := blockingCollector{Block: make(chan struct{}), CollectStarted: make(chan struct{}, 1)} + reg.MustRegister(c) + + handler.ServeHTTP(w, request) + + if got, want := w.Code, http.StatusServiceUnavailable; got != want { + t.Errorf("got HTTP status code %d, want %d", got, want) + } + if got, want := w.Body.String(), "Exceeded configured timeout of 1ms.\n"; got != want { + t.Errorf("got body %q, want %q", got, want) + } + + close(c.Block) // To not leak a goroutine. +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client.go new file mode 100644 index 0000000000..86fd564470 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client.go @@ -0,0 +1,97 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package promhttp + +import ( + "net/http" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +// The RoundTripperFunc type is an adapter to allow the use of ordinary +// functions as RoundTrippers. If f is a function with the appropriate +// signature, RountTripperFunc(f) is a RoundTripper that calls f. +type RoundTripperFunc func(req *http.Request) (*http.Response, error) + +// RoundTrip implements the RoundTripper interface. +func (rt RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { + return rt(r) +} + +// InstrumentRoundTripperInFlight is a middleware that wraps the provided +// http.RoundTripper. It sets the provided prometheus.Gauge to the number of +// requests currently handled by the wrapped http.RoundTripper. +// +// See the example for ExampleInstrumentRoundTripperDuration for example usage. +func InstrumentRoundTripperInFlight(gauge prometheus.Gauge, next http.RoundTripper) RoundTripperFunc { + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + gauge.Inc() + defer gauge.Dec() + return next.RoundTrip(r) + }) +} + +// InstrumentRoundTripperCounter is a middleware that wraps the provided +// http.RoundTripper to observe the request result with the provided CounterVec. +// The CounterVec must have zero, one, or two non-const non-curried labels. For +// those, the only allowed label names are "code" and "method". The function +// panics otherwise. Partitioning of the CounterVec happens by HTTP status code +// and/or HTTP method if the respective instance label names are present in the +// CounterVec. For unpartitioned counting, use a CounterVec with zero labels. +// +// If the wrapped RoundTripper panics or returns a non-nil error, the Counter +// is not incremented. +// +// See the example for ExampleInstrumentRoundTripperDuration for example usage. +func InstrumentRoundTripperCounter(counter *prometheus.CounterVec, next http.RoundTripper) RoundTripperFunc { + code, method := checkLabels(counter) + + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + resp, err := next.RoundTrip(r) + if err == nil { + counter.With(labels(code, method, r.Method, resp.StatusCode)).Inc() + } + return resp, err + }) +} + +// InstrumentRoundTripperDuration is a middleware that wraps the provided +// http.RoundTripper to observe the request duration with the provided +// ObserverVec. The ObserverVec must have zero, one, or two non-const +// non-curried labels. For those, the only allowed label names are "code" and +// "method". The function panics otherwise. The Observe method of the Observer +// in the ObserverVec is called with the request duration in +// seconds. Partitioning happens by HTTP status code and/or HTTP method if the +// respective instance label names are present in the ObserverVec. For +// unpartitioned observations, use an ObserverVec with zero labels. Note that +// partitioning of Histograms is expensive and should be used judiciously. +// +// If the wrapped RoundTripper panics or returns a non-nil error, no values are +// reported. +// +// Note that this method is only guaranteed to never observe negative durations +// if used with Go1.9+. +func InstrumentRoundTripperDuration(obs prometheus.ObserverVec, next http.RoundTripper) RoundTripperFunc { + code, method := checkLabels(obs) + + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + start := time.Now() + resp, err := next.RoundTrip(r) + if err == nil { + obs.With(labels(code, method, r.Method, resp.StatusCode)).Observe(time.Since(start).Seconds()) + } + return resp, err + }) +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client_1_8.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client_1_8.go new file mode 100644 index 0000000000..a034d1ec0f --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client_1_8.go @@ -0,0 +1,144 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build go1.8 + +package promhttp + +import ( + "context" + "crypto/tls" + "net/http" + "net/http/httptrace" + "time" +) + +// InstrumentTrace is used to offer flexibility in instrumenting the available +// httptrace.ClientTrace hook functions. Each function is passed a float64 +// representing the time in seconds since the start of the http request. A user +// may choose to use separately buckets Histograms, or implement custom +// instance labels on a per function basis. +type InstrumentTrace struct { + GotConn func(float64) + PutIdleConn func(float64) + GotFirstResponseByte func(float64) + Got100Continue func(float64) + DNSStart func(float64) + DNSDone func(float64) + ConnectStart func(float64) + ConnectDone func(float64) + TLSHandshakeStart func(float64) + TLSHandshakeDone func(float64) + WroteHeaders func(float64) + Wait100Continue func(float64) + WroteRequest func(float64) +} + +// InstrumentRoundTripperTrace is a middleware that wraps the provided +// RoundTripper and reports times to hook functions provided in the +// InstrumentTrace struct. Hook functions that are not present in the provided +// InstrumentTrace struct are ignored. Times reported to the hook functions are +// time since the start of the request. Only with Go1.9+, those times are +// guaranteed to never be negative. (Earlier Go versions are not using a +// monotonic clock.) Note that partitioning of Histograms is expensive and +// should be used judiciously. +// +// For hook functions that receive an error as an argument, no observations are +// made in the event of a non-nil error value. +// +// See the example for ExampleInstrumentRoundTripperDuration for example usage. +func InstrumentRoundTripperTrace(it *InstrumentTrace, next http.RoundTripper) RoundTripperFunc { + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + start := time.Now() + + trace := &httptrace.ClientTrace{ + GotConn: func(_ httptrace.GotConnInfo) { + if it.GotConn != nil { + it.GotConn(time.Since(start).Seconds()) + } + }, + PutIdleConn: func(err error) { + if err != nil { + return + } + if it.PutIdleConn != nil { + it.PutIdleConn(time.Since(start).Seconds()) + } + }, + DNSStart: func(_ httptrace.DNSStartInfo) { + if it.DNSStart != nil { + it.DNSStart(time.Since(start).Seconds()) + } + }, + DNSDone: func(_ httptrace.DNSDoneInfo) { + if it.DNSDone != nil { + it.DNSDone(time.Since(start).Seconds()) + } + }, + ConnectStart: func(_, _ string) { + if it.ConnectStart != nil { + it.ConnectStart(time.Since(start).Seconds()) + } + }, + ConnectDone: func(_, _ string, err error) { + if err != nil { + return + } + if it.ConnectDone != nil { + it.ConnectDone(time.Since(start).Seconds()) + } + }, + GotFirstResponseByte: func() { + if it.GotFirstResponseByte != nil { + it.GotFirstResponseByte(time.Since(start).Seconds()) + } + }, + Got100Continue: func() { + if it.Got100Continue != nil { + it.Got100Continue(time.Since(start).Seconds()) + } + }, + TLSHandshakeStart: func() { + if it.TLSHandshakeStart != nil { + it.TLSHandshakeStart(time.Since(start).Seconds()) + } + }, + TLSHandshakeDone: func(_ tls.ConnectionState, err error) { + if err != nil { + return + } + if it.TLSHandshakeDone != nil { + it.TLSHandshakeDone(time.Since(start).Seconds()) + } + }, + WroteHeaders: func() { + if it.WroteHeaders != nil { + it.WroteHeaders(time.Since(start).Seconds()) + } + }, + Wait100Continue: func() { + if it.Wait100Continue != nil { + it.Wait100Continue(time.Since(start).Seconds()) + } + }, + WroteRequest: func(_ httptrace.WroteRequestInfo) { + if it.WroteRequest != nil { + it.WroteRequest(time.Since(start).Seconds()) + } + }, + } + r = r.WithContext(httptrace.WithClientTrace(context.Background(), trace)) + + return next.RoundTrip(r) + }) +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client_1_8_test.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client_1_8_test.go new file mode 100644 index 0000000000..7e3f5229fe --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client_1_8_test.go @@ -0,0 +1,195 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build go1.8 + +package promhttp + +import ( + "log" + "net/http" + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func TestClientMiddlewareAPI(t *testing.T) { + client := http.DefaultClient + client.Timeout = 1 * time.Second + + reg := prometheus.NewRegistry() + + inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "client_in_flight_requests", + Help: "A gauge of in-flight requests for the wrapped client.", + }) + + counter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "client_api_requests_total", + Help: "A counter for requests from the wrapped client.", + }, + []string{"code", "method"}, + ) + + dnsLatencyVec := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "dns_duration_seconds", + Help: "Trace dns latency histogram.", + Buckets: []float64{.005, .01, .025, .05}, + }, + []string{"event"}, + ) + + tlsLatencyVec := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "tls_duration_seconds", + Help: "Trace tls latency histogram.", + Buckets: []float64{.05, .1, .25, .5}, + }, + []string{"event"}, + ) + + histVec := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "request_duration_seconds", + Help: "A histogram of request latencies.", + Buckets: prometheus.DefBuckets, + }, + []string{"method"}, + ) + + reg.MustRegister(counter, tlsLatencyVec, dnsLatencyVec, histVec, inFlightGauge) + + trace := &InstrumentTrace{ + DNSStart: func(t float64) { + dnsLatencyVec.WithLabelValues("dns_start") + }, + DNSDone: func(t float64) { + dnsLatencyVec.WithLabelValues("dns_done") + }, + TLSHandshakeStart: func(t float64) { + tlsLatencyVec.WithLabelValues("tls_handshake_start") + }, + TLSHandshakeDone: func(t float64) { + tlsLatencyVec.WithLabelValues("tls_handshake_done") + }, + } + + client.Transport = InstrumentRoundTripperInFlight(inFlightGauge, + InstrumentRoundTripperCounter(counter, + InstrumentRoundTripperTrace(trace, + InstrumentRoundTripperDuration(histVec, http.DefaultTransport), + ), + ), + ) + + resp, err := client.Get("http://google.com") + if err != nil { + t.Fatalf("%v", err) + } + defer resp.Body.Close() +} + +func ExampleInstrumentRoundTripperDuration() { + client := http.DefaultClient + client.Timeout = 1 * time.Second + + inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "client_in_flight_requests", + Help: "A gauge of in-flight requests for the wrapped client.", + }) + + counter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "client_api_requests_total", + Help: "A counter for requests from the wrapped client.", + }, + []string{"code", "method"}, + ) + + // dnsLatencyVec uses custom buckets based on expected dns durations. + // It has an instance label "event", which is set in the + // DNSStart and DNSDonehook functions defined in the + // InstrumentTrace struct below. + dnsLatencyVec := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "dns_duration_seconds", + Help: "Trace dns latency histogram.", + Buckets: []float64{.005, .01, .025, .05}, + }, + []string{"event"}, + ) + + // tlsLatencyVec uses custom buckets based on expected tls durations. + // It has an instance label "event", which is set in the + // TLSHandshakeStart and TLSHandshakeDone hook functions defined in the + // InstrumentTrace struct below. + tlsLatencyVec := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "tls_duration_seconds", + Help: "Trace tls latency histogram.", + Buckets: []float64{.05, .1, .25, .5}, + }, + []string{"event"}, + ) + + // histVec has no labels, making it a zero-dimensional ObserverVec. + histVec := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "request_duration_seconds", + Help: "A histogram of request latencies.", + Buckets: prometheus.DefBuckets, + }, + []string{}, + ) + + // Register all of the metrics in the standard registry. + prometheus.MustRegister(counter, tlsLatencyVec, dnsLatencyVec, histVec, inFlightGauge) + + // Define functions for the available httptrace.ClientTrace hook + // functions that we want to instrument. + trace := &InstrumentTrace{ + DNSStart: func(t float64) { + dnsLatencyVec.WithLabelValues("dns_start") + }, + DNSDone: func(t float64) { + dnsLatencyVec.WithLabelValues("dns_done") + }, + TLSHandshakeStart: func(t float64) { + tlsLatencyVec.WithLabelValues("tls_handshake_start") + }, + TLSHandshakeDone: func(t float64) { + tlsLatencyVec.WithLabelValues("tls_handshake_done") + }, + } + + // Wrap the default RoundTripper with middleware. + roundTripper := InstrumentRoundTripperInFlight(inFlightGauge, + InstrumentRoundTripperCounter(counter, + InstrumentRoundTripperTrace(trace, + InstrumentRoundTripperDuration(histVec, http.DefaultTransport), + ), + ), + ) + + // Set the RoundTripper on our client. + client.Transport = roundTripper + + resp, err := client.Get("http://google.com") + if err != nil { + log.Printf("error: %v", err) + } + defer resp.Body.Close() +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_server.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_server.go new file mode 100644 index 0000000000..9db2438053 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_server.go @@ -0,0 +1,447 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package promhttp + +import ( + "errors" + "net/http" + "strconv" + "strings" + "time" + + dto "github.com/prometheus/client_model/go" + + "github.com/prometheus/client_golang/prometheus" +) + +// magicString is used for the hacky label test in checkLabels. Remove once fixed. +const magicString = "zZgWfBxLqvG8kc8IMv3POi2Bb0tZI3vAnBx+gBaFi9FyPzB/CzKUer1yufDa" + +// InstrumentHandlerInFlight is a middleware that wraps the provided +// http.Handler. It sets the provided prometheus.Gauge to the number of +// requests currently handled by the wrapped http.Handler. +// +// See the example for InstrumentHandlerDuration for example usage. +func InstrumentHandlerInFlight(g prometheus.Gauge, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + g.Inc() + defer g.Dec() + next.ServeHTTP(w, r) + }) +} + +// InstrumentHandlerDuration is a middleware that wraps the provided +// http.Handler to observe the request duration with the provided ObserverVec. +// The ObserverVec must have zero, one, or two non-const non-curried labels. For +// those, the only allowed label names are "code" and "method". The function +// panics otherwise. The Observe method of the Observer in the ObserverVec is +// called with the request duration in seconds. Partitioning happens by HTTP +// status code and/or HTTP method if the respective instance label names are +// present in the ObserverVec. For unpartitioned observations, use an +// ObserverVec with zero labels. Note that partitioning of Histograms is +// expensive and should be used judiciously. +// +// If the wrapped Handler does not set a status code, a status code of 200 is assumed. +// +// If the wrapped Handler panics, no values are reported. +// +// Note that this method is only guaranteed to never observe negative durations +// if used with Go1.9+. +func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler) http.HandlerFunc { + code, method := checkLabels(obs) + + if code { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + now := time.Now() + d := newDelegator(w, nil) + next.ServeHTTP(d, r) + + obs.With(labels(code, method, r.Method, d.Status())).Observe(time.Since(now).Seconds()) + }) + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + now := time.Now() + next.ServeHTTP(w, r) + obs.With(labels(code, method, r.Method, 0)).Observe(time.Since(now).Seconds()) + }) +} + +// InstrumentHandlerCounter is a middleware that wraps the provided http.Handler +// to observe the request result with the provided CounterVec. The CounterVec +// must have zero, one, or two non-const non-curried labels. For those, the only +// allowed label names are "code" and "method". The function panics +// otherwise. Partitioning of the CounterVec happens by HTTP status code and/or +// HTTP method if the respective instance label names are present in the +// CounterVec. For unpartitioned counting, use a CounterVec with zero labels. +// +// If the wrapped Handler does not set a status code, a status code of 200 is assumed. +// +// If the wrapped Handler panics, the Counter is not incremented. +// +// See the example for InstrumentHandlerDuration for example usage. +func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler) http.HandlerFunc { + code, method := checkLabels(counter) + + if code { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + d := newDelegator(w, nil) + next.ServeHTTP(d, r) + counter.With(labels(code, method, r.Method, d.Status())).Inc() + }) + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + counter.With(labels(code, method, r.Method, 0)).Inc() + }) +} + +// InstrumentHandlerTimeToWriteHeader is a middleware that wraps the provided +// http.Handler to observe with the provided ObserverVec the request duration +// until the response headers are written. The ObserverVec must have zero, one, +// or two non-const non-curried labels. For those, the only allowed label names +// are "code" and "method". The function panics otherwise. The Observe method of +// the Observer in the ObserverVec is called with the request duration in +// seconds. Partitioning happens by HTTP status code and/or HTTP method if the +// respective instance label names are present in the ObserverVec. For +// unpartitioned observations, use an ObserverVec with zero labels. Note that +// partitioning of Histograms is expensive and should be used judiciously. +// +// If the wrapped Handler panics before calling WriteHeader, no value is +// reported. +// +// Note that this method is only guaranteed to never observe negative durations +// if used with Go1.9+. +// +// See the example for InstrumentHandlerDuration for example usage. +func InstrumentHandlerTimeToWriteHeader(obs prometheus.ObserverVec, next http.Handler) http.HandlerFunc { + code, method := checkLabels(obs) + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + now := time.Now() + d := newDelegator(w, func(status int) { + obs.With(labels(code, method, r.Method, status)).Observe(time.Since(now).Seconds()) + }) + next.ServeHTTP(d, r) + }) +} + +// InstrumentHandlerRequestSize is a middleware that wraps the provided +// http.Handler to observe the request size with the provided ObserverVec. The +// ObserverVec must have zero, one, or two non-const non-curried labels. For +// those, the only allowed label names are "code" and "method". The function +// panics otherwise. The Observe method of the Observer in the ObserverVec is +// called with the request size in bytes. Partitioning happens by HTTP status +// code and/or HTTP method if the respective instance label names are present in +// the ObserverVec. For unpartitioned observations, use an ObserverVec with zero +// labels. Note that partitioning of Histograms is expensive and should be used +// judiciously. +// +// If the wrapped Handler does not set a status code, a status code of 200 is assumed. +// +// If the wrapped Handler panics, no values are reported. +// +// See the example for InstrumentHandlerDuration for example usage. +func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler) http.HandlerFunc { + code, method := checkLabels(obs) + + if code { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + d := newDelegator(w, nil) + next.ServeHTTP(d, r) + size := computeApproximateRequestSize(r) + obs.With(labels(code, method, r.Method, d.Status())).Observe(float64(size)) + }) + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + size := computeApproximateRequestSize(r) + obs.With(labels(code, method, r.Method, 0)).Observe(float64(size)) + }) +} + +// InstrumentHandlerResponseSize is a middleware that wraps the provided +// http.Handler to observe the response size with the provided ObserverVec. The +// ObserverVec must have zero, one, or two non-const non-curried labels. For +// those, the only allowed label names are "code" and "method". The function +// panics otherwise. The Observe method of the Observer in the ObserverVec is +// called with the response size in bytes. Partitioning happens by HTTP status +// code and/or HTTP method if the respective instance label names are present in +// the ObserverVec. For unpartitioned observations, use an ObserverVec with zero +// labels. Note that partitioning of Histograms is expensive and should be used +// judiciously. +// +// If the wrapped Handler does not set a status code, a status code of 200 is assumed. +// +// If the wrapped Handler panics, no values are reported. +// +// See the example for InstrumentHandlerDuration for example usage. +func InstrumentHandlerResponseSize(obs prometheus.ObserverVec, next http.Handler) http.Handler { + code, method := checkLabels(obs) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + d := newDelegator(w, nil) + next.ServeHTTP(d, r) + obs.With(labels(code, method, r.Method, d.Status())).Observe(float64(d.Written())) + }) +} + +func checkLabels(c prometheus.Collector) (code bool, method bool) { + // TODO(beorn7): Remove this hacky way to check for instance labels + // once Descriptors can have their dimensionality queried. + var ( + desc *prometheus.Desc + m prometheus.Metric + pm dto.Metric + lvs []string + ) + + // Get the Desc from the Collector. + descc := make(chan *prometheus.Desc, 1) + c.Describe(descc) + + select { + case desc = <-descc: + default: + panic("no description provided by collector") + } + select { + case <-descc: + panic("more than one description provided by collector") + default: + } + + close(descc) + + // Create a ConstMetric with the Desc. Since we don't know how many + // variable labels there are, try for as long as it needs. + for err := errors.New("dummy"); err != nil; lvs = append(lvs, magicString) { + m, err = prometheus.NewConstMetric(desc, prometheus.UntypedValue, 0, lvs...) + } + + // Write out the metric into a proto message and look at the labels. + // If the value is not the magicString, it is a constLabel, which doesn't interest us. + // If the label is curried, it doesn't interest us. + // In all other cases, only "code" or "method" is allowed. + if err := m.Write(&pm); err != nil { + panic("error checking metric for labels") + } + for _, label := range pm.Label { + name, value := label.GetName(), label.GetValue() + if value != magicString || isLabelCurried(c, name) { + continue + } + switch name { + case "code": + code = true + case "method": + method = true + default: + panic("metric partitioned with non-supported labels") + } + } + return +} + +func isLabelCurried(c prometheus.Collector, label string) bool { + // This is even hackier than the label test above. + // We essentially try to curry again and see if it works. + // But for that, we need to type-convert to the two + // types we use here, ObserverVec or *CounterVec. + switch v := c.(type) { + case *prometheus.CounterVec: + if _, err := v.CurryWith(prometheus.Labels{label: "dummy"}); err == nil { + return false + } + case prometheus.ObserverVec: + if _, err := v.CurryWith(prometheus.Labels{label: "dummy"}); err == nil { + return false + } + default: + panic("unsupported metric vec type") + } + return true +} + +// emptyLabels is a one-time allocation for non-partitioned metrics to avoid +// unnecessary allocations on each request. +var emptyLabels = prometheus.Labels{} + +func labels(code, method bool, reqMethod string, status int) prometheus.Labels { + if !(code || method) { + return emptyLabels + } + labels := prometheus.Labels{} + + if code { + labels["code"] = sanitizeCode(status) + } + if method { + labels["method"] = sanitizeMethod(reqMethod) + } + + return labels +} + +func computeApproximateRequestSize(r *http.Request) int { + s := 0 + if r.URL != nil { + s += len(r.URL.String()) + } + + s += len(r.Method) + s += len(r.Proto) + for name, values := range r.Header { + s += len(name) + for _, value := range values { + s += len(value) + } + } + s += len(r.Host) + + // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. + + if r.ContentLength != -1 { + s += int(r.ContentLength) + } + return s +} + +func sanitizeMethod(m string) string { + switch m { + case "GET", "get": + return "get" + case "PUT", "put": + return "put" + case "HEAD", "head": + return "head" + case "POST", "post": + return "post" + case "DELETE", "delete": + return "delete" + case "CONNECT", "connect": + return "connect" + case "OPTIONS", "options": + return "options" + case "NOTIFY", "notify": + return "notify" + default: + return strings.ToLower(m) + } +} + +// If the wrapped http.Handler has not set a status code, i.e. the value is +// currently 0, santizeCode will return 200, for consistency with behavior in +// the stdlib. +func sanitizeCode(s int) string { + switch s { + case 100: + return "100" + case 101: + return "101" + + case 200, 0: + return "200" + case 201: + return "201" + case 202: + return "202" + case 203: + return "203" + case 204: + return "204" + case 205: + return "205" + case 206: + return "206" + + case 300: + return "300" + case 301: + return "301" + case 302: + return "302" + case 304: + return "304" + case 305: + return "305" + case 307: + return "307" + + case 400: + return "400" + case 401: + return "401" + case 402: + return "402" + case 403: + return "403" + case 404: + return "404" + case 405: + return "405" + case 406: + return "406" + case 407: + return "407" + case 408: + return "408" + case 409: + return "409" + case 410: + return "410" + case 411: + return "411" + case 412: + return "412" + case 413: + return "413" + case 414: + return "414" + case 415: + return "415" + case 416: + return "416" + case 417: + return "417" + case 418: + return "418" + + case 500: + return "500" + case 501: + return "501" + case 502: + return "502" + case 503: + return "503" + case 504: + return "504" + case 505: + return "505" + + case 428: + return "428" + case 429: + return "429" + case 431: + return "431" + case 511: + return "511" + + default: + return strconv.Itoa(s) + } +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_server_test.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_server_test.go new file mode 100644 index 0000000000..716c6f45e0 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_server_test.go @@ -0,0 +1,401 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package promhttp + +import ( + "io" + "log" + "net/http" + "net/http/httptest" + "testing" + + "github.com/prometheus/client_golang/prometheus" +) + +func TestLabelCheck(t *testing.T) { + scenarios := map[string]struct { + varLabels []string + constLabels []string + curriedLabels []string + ok bool + }{ + "empty": { + varLabels: []string{}, + constLabels: []string{}, + curriedLabels: []string{}, + ok: true, + }, + "code as single var label": { + varLabels: []string{"code"}, + constLabels: []string{}, + curriedLabels: []string{}, + ok: true, + }, + "method as single var label": { + varLabels: []string{"method"}, + constLabels: []string{}, + curriedLabels: []string{}, + ok: true, + }, + "cade and method as var labels": { + varLabels: []string{"method", "code"}, + constLabels: []string{}, + curriedLabels: []string{}, + ok: true, + }, + "valid case with all labels used": { + varLabels: []string{"code", "method"}, + constLabels: []string{"foo", "bar"}, + curriedLabels: []string{"dings", "bums"}, + ok: true, + }, + "unsupported var label": { + varLabels: []string{"foo"}, + constLabels: []string{}, + curriedLabels: []string{}, + ok: false, + }, + "mixed var labels": { + varLabels: []string{"method", "foo", "code"}, + constLabels: []string{}, + curriedLabels: []string{}, + ok: false, + }, + "unsupported var label but curried": { + varLabels: []string{}, + constLabels: []string{}, + curriedLabels: []string{"foo"}, + ok: true, + }, + "mixed var labels but unsupported curried": { + varLabels: []string{"code", "method"}, + constLabels: []string{}, + curriedLabels: []string{"foo"}, + ok: true, + }, + "supported label as const and curry": { + varLabels: []string{}, + constLabels: []string{"code"}, + curriedLabels: []string{"method"}, + ok: true, + }, + "supported label as const and curry with unsupported as var": { + varLabels: []string{"foo"}, + constLabels: []string{"code"}, + curriedLabels: []string{"method"}, + ok: false, + }, + } + + for name, sc := range scenarios { + t.Run(name, func(t *testing.T) { + constLabels := prometheus.Labels{} + for _, l := range sc.constLabels { + constLabels[l] = "dummy" + } + c := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "c", + Help: "c help", + ConstLabels: constLabels, + }, + append(sc.varLabels, sc.curriedLabels...), + ) + o := prometheus.ObserverVec(prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "c", + Help: "c help", + ConstLabels: constLabels, + }, + append(sc.varLabels, sc.curriedLabels...), + )) + for _, l := range sc.curriedLabels { + c = c.MustCurryWith(prometheus.Labels{l: "dummy"}) + o = o.MustCurryWith(prometheus.Labels{l: "dummy"}) + } + + func() { + defer func() { + if err := recover(); err != nil { + if sc.ok { + t.Error("unexpected panic:", err) + } + } else if !sc.ok { + t.Error("expected panic") + } + }() + InstrumentHandlerCounter(c, nil) + }() + func() { + defer func() { + if err := recover(); err != nil { + if sc.ok { + t.Error("unexpected panic:", err) + } + } else if !sc.ok { + t.Error("expected panic") + } + }() + InstrumentHandlerDuration(o, nil) + }() + if sc.ok { + // Test if wantCode and wantMethod were detected correctly. + var wantCode, wantMethod bool + for _, l := range sc.varLabels { + if l == "code" { + wantCode = true + } + if l == "method" { + wantMethod = true + } + } + gotCode, gotMethod := checkLabels(c) + if gotCode != wantCode { + t.Errorf("wanted code=%t for counter, got code=%t", wantCode, gotCode) + } + if gotMethod != wantMethod { + t.Errorf("wanted method=%t for counter, got method=%t", wantMethod, gotMethod) + } + gotCode, gotMethod = checkLabels(o) + if gotCode != wantCode { + t.Errorf("wanted code=%t for observer, got code=%t", wantCode, gotCode) + } + if gotMethod != wantMethod { + t.Errorf("wanted method=%t for observer, got method=%t", wantMethod, gotMethod) + } + } + }) + } +} + +func TestMiddlewareAPI(t *testing.T) { + reg := prometheus.NewRegistry() + + inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "in_flight_requests", + Help: "A gauge of requests currently being served by the wrapped handler.", + }) + + counter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "api_requests_total", + Help: "A counter for requests to the wrapped handler.", + }, + []string{"code", "method"}, + ) + + histVec := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "response_duration_seconds", + Help: "A histogram of request latencies.", + Buckets: prometheus.DefBuckets, + ConstLabels: prometheus.Labels{"handler": "api"}, + }, + []string{"method"}, + ) + + writeHeaderVec := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "write_header_duration_seconds", + Help: "A histogram of time to first write latencies.", + Buckets: prometheus.DefBuckets, + ConstLabels: prometheus.Labels{"handler": "api"}, + }, + []string{}, + ) + + responseSize := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "push_request_size_bytes", + Help: "A histogram of request sizes for requests.", + Buckets: []float64{200, 500, 900, 1500}, + }, + []string{}, + ) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK")) + }) + + reg.MustRegister(inFlightGauge, counter, histVec, responseSize, writeHeaderVec) + + chain := InstrumentHandlerInFlight(inFlightGauge, + InstrumentHandlerCounter(counter, + InstrumentHandlerDuration(histVec, + InstrumentHandlerTimeToWriteHeader(writeHeaderVec, + InstrumentHandlerResponseSize(responseSize, handler), + ), + ), + ), + ) + + r, _ := http.NewRequest("GET", "www.example.com", nil) + w := httptest.NewRecorder() + chain.ServeHTTP(w, r) +} + +func TestInstrumentTimeToFirstWrite(t *testing.T) { + var i int + dobs := &responseWriterDelegator{ + ResponseWriter: httptest.NewRecorder(), + observeWriteHeader: func(status int) { + i = status + }, + } + d := newDelegator(dobs, nil) + + d.WriteHeader(http.StatusOK) + + if i != http.StatusOK { + t.Fatalf("failed to execute observeWriteHeader") + } +} + +// testResponseWriter is an http.ResponseWriter that also implements +// http.CloseNotifier, http.Flusher, and io.ReaderFrom. +type testResponseWriter struct { + closeNotifyCalled, flushCalled, readFromCalled bool +} + +func (t *testResponseWriter) Header() http.Header { return nil } +func (t *testResponseWriter) Write([]byte) (int, error) { return 0, nil } +func (t *testResponseWriter) WriteHeader(int) {} +func (t *testResponseWriter) CloseNotify() <-chan bool { + t.closeNotifyCalled = true + return nil +} +func (t *testResponseWriter) Flush() { t.flushCalled = true } +func (t *testResponseWriter) ReadFrom(io.Reader) (int64, error) { + t.readFromCalled = true + return 0, nil +} + +// testFlusher is an http.ResponseWriter that also implements http.Flusher. +type testFlusher struct { + flushCalled bool +} + +func (t *testFlusher) Header() http.Header { return nil } +func (t *testFlusher) Write([]byte) (int, error) { return 0, nil } +func (t *testFlusher) WriteHeader(int) {} +func (t *testFlusher) Flush() { t.flushCalled = true } + +func TestInterfaceUpgrade(t *testing.T) { + w := &testResponseWriter{} + d := newDelegator(w, nil) + d.(http.CloseNotifier).CloseNotify() + if !w.closeNotifyCalled { + t.Error("CloseNotify not called") + } + d.(http.Flusher).Flush() + if !w.flushCalled { + t.Error("Flush not called") + } + d.(io.ReaderFrom).ReadFrom(nil) + if !w.readFromCalled { + t.Error("ReadFrom not called") + } + if _, ok := d.(http.Hijacker); ok { + t.Error("delegator unexpectedly implements http.Hijacker") + } + + f := &testFlusher{} + d = newDelegator(f, nil) + if _, ok := d.(http.CloseNotifier); ok { + t.Error("delegator unexpectedly implements http.CloseNotifier") + } + d.(http.Flusher).Flush() + if !w.flushCalled { + t.Error("Flush not called") + } + if _, ok := d.(io.ReaderFrom); ok { + t.Error("delegator unexpectedly implements io.ReaderFrom") + } + if _, ok := d.(http.Hijacker); ok { + t.Error("delegator unexpectedly implements http.Hijacker") + } +} + +func ExampleInstrumentHandlerDuration() { + inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "in_flight_requests", + Help: "A gauge of requests currently being served by the wrapped handler.", + }) + + counter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "api_requests_total", + Help: "A counter for requests to the wrapped handler.", + }, + []string{"code", "method"}, + ) + + // duration is partitioned by the HTTP method and handler. It uses custom + // buckets based on the expected request duration. + duration := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "request_duration_seconds", + Help: "A histogram of latencies for requests.", + Buckets: []float64{.25, .5, 1, 2.5, 5, 10}, + }, + []string{"handler", "method"}, + ) + + // responseSize has no labels, making it a zero-dimensional + // ObserverVec. + responseSize := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "response_size_bytes", + Help: "A histogram of response sizes for requests.", + Buckets: []float64{200, 500, 900, 1500}, + }, + []string{}, + ) + + // Create the handlers that will be wrapped by the middleware. + pushHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Push")) + }) + pullHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Pull")) + }) + + // Register all of the metrics in the standard registry. + prometheus.MustRegister(inFlightGauge, counter, duration, responseSize) + + // Instrument the handlers with all the metrics, injecting the "handler" + // label by currying. + pushChain := InstrumentHandlerInFlight(inFlightGauge, + InstrumentHandlerDuration(duration.MustCurryWith(prometheus.Labels{"handler": "push"}), + InstrumentHandlerCounter(counter, + InstrumentHandlerResponseSize(responseSize, pushHandler), + ), + ), + ) + pullChain := InstrumentHandlerInFlight(inFlightGauge, + InstrumentHandlerDuration(duration.MustCurryWith(prometheus.Labels{"handler": "pull"}), + InstrumentHandlerCounter(counter, + InstrumentHandlerResponseSize(responseSize, pullHandler), + ), + ), + ) + + http.Handle("/metrics", Handler()) + http.Handle("/push", pushChain) + http.Handle("/pull", pullChain) + + if err := http.ListenAndServe(":3000", nil); err != nil { + log.Fatal(err) + } +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/push/deprecated.go b/vendor/github.com/prometheus/client_golang/prometheus/push/deprecated.go new file mode 100644 index 0000000000..3d62b5725d --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/push/deprecated.go @@ -0,0 +1,172 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package push + +// This file contains only deprecated code. Remove after v0.9 is released. + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + + "github.com/prometheus/common/expfmt" + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +// FromGatherer triggers a metric collection by the provided Gatherer (which is +// usually implemented by a prometheus.Registry) and pushes all gathered metrics +// to the Pushgateway specified by url, using the provided job name and the +// (optional) further grouping labels (the grouping map may be nil). See the +// Pushgateway documentation for detailed implications of the job and other +// grouping labels. Neither the job name nor any grouping label value may +// contain a "/". The metrics pushed must not contain a job label of their own +// nor any of the grouping labels. +// +// You can use just host:port or ip:port as url, in which case 'http://' is +// added automatically. You can also include the schema in the URL. However, do +// not include the '/metrics/jobs/...' part. +// +// Note that all previously pushed metrics with the same job and other grouping +// labels will be replaced with the metrics pushed by this call. (It uses HTTP +// method 'PUT' to push to the Pushgateway.) +// +// Deprecated: Please use a Pusher created with New instead. +func FromGatherer(job string, grouping map[string]string, url string, g prometheus.Gatherer) error { + return push(job, grouping, url, g, "PUT") +} + +// AddFromGatherer works like FromGatherer, but only previously pushed metrics +// with the same name (and the same job and other grouping labels) will be +// replaced. (It uses HTTP method 'POST' to push to the Pushgateway.) +// +// Deprecated: Please use a Pusher created with New instead. +func AddFromGatherer(job string, grouping map[string]string, url string, g prometheus.Gatherer) error { + return push(job, grouping, url, g, "POST") +} + +func push(job string, grouping map[string]string, pushURL string, g prometheus.Gatherer, method string) error { + if !strings.Contains(pushURL, "://") { + pushURL = "http://" + pushURL + } + if strings.HasSuffix(pushURL, "/") { + pushURL = pushURL[:len(pushURL)-1] + } + + if strings.Contains(job, "/") { + return fmt.Errorf("job contains '/': %s", job) + } + urlComponents := []string{url.QueryEscape(job)} + for ln, lv := range grouping { + if !model.LabelName(ln).IsValid() { + return fmt.Errorf("grouping label has invalid name: %s", ln) + } + if strings.Contains(lv, "/") { + return fmt.Errorf("value of grouping label %s contains '/': %s", ln, lv) + } + urlComponents = append(urlComponents, ln, lv) + } + pushURL = fmt.Sprintf("%s/metrics/job/%s", pushURL, strings.Join(urlComponents, "/")) + + mfs, err := g.Gather() + if err != nil { + return err + } + buf := &bytes.Buffer{} + enc := expfmt.NewEncoder(buf, expfmt.FmtProtoDelim) + // Check for pre-existing grouping labels: + for _, mf := range mfs { + for _, m := range mf.GetMetric() { + for _, l := range m.GetLabel() { + if l.GetName() == "job" { + return fmt.Errorf("pushed metric %s (%s) already contains a job label", mf.GetName(), m) + } + if _, ok := grouping[l.GetName()]; ok { + return fmt.Errorf( + "pushed metric %s (%s) already contains grouping label %s", + mf.GetName(), m, l.GetName(), + ) + } + } + } + enc.Encode(mf) + } + req, err := http.NewRequest(method, pushURL, buf) + if err != nil { + return err + } + req.Header.Set(contentTypeHeader, string(expfmt.FmtProtoDelim)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != 202 { + body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only. + return fmt.Errorf("unexpected status code %d while pushing to %s: %s", resp.StatusCode, pushURL, body) + } + return nil +} + +// Collectors works like FromGatherer, but it does not use a Gatherer. Instead, +// it collects from the provided collectors directly. It is a convenient way to +// push only a few metrics. +// +// Deprecated: Please use a Pusher created with New instead. +func Collectors(job string, grouping map[string]string, url string, collectors ...prometheus.Collector) error { + return pushCollectors(job, grouping, url, "PUT", collectors...) +} + +// AddCollectors works like AddFromGatherer, but it does not use a Gatherer. +// Instead, it collects from the provided collectors directly. It is a +// convenient way to push only a few metrics. +// +// Deprecated: Please use a Pusher created with New instead. +func AddCollectors(job string, grouping map[string]string, url string, collectors ...prometheus.Collector) error { + return pushCollectors(job, grouping, url, "POST", collectors...) +} + +func pushCollectors(job string, grouping map[string]string, url, method string, collectors ...prometheus.Collector) error { + r := prometheus.NewRegistry() + for _, collector := range collectors { + if err := r.Register(collector); err != nil { + return err + } + } + return push(job, grouping, url, r, method) +} + +// HostnameGroupingKey returns a label map with the only entry +// {instance=""}. This can be conveniently used as the grouping +// parameter if metrics should be pushed with the hostname as label. The +// returned map is created upon each call so that the caller is free to add more +// labels to the map. +// +// Deprecated: Usually, metrics pushed to the Pushgateway should not be +// host-centric. (You would use https://github.com/prometheus/node_exporter in +// that case.) If you have the need to add the hostname to the grouping key, you +// are probably doing something wrong. See +// https://prometheus.io/docs/practices/pushing/ for details. +func HostnameGroupingKey() map[string]string { + hostname, err := os.Hostname() + if err != nil { + return map[string]string{"instance": "unknown"} + } + return map[string]string{"instance": hostname} +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/push/example_add_from_gatherer_test.go b/vendor/github.com/prometheus/client_golang/prometheus/push/example_add_from_gatherer_test.go new file mode 100644 index 0000000000..dd22b526a2 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/push/example_add_from_gatherer_test.go @@ -0,0 +1,80 @@ +// Copyright 2016 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package push_test + +import ( + "fmt" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/push" +) + +var ( + completionTime = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "db_backup_last_completion_timestamp_seconds", + Help: "The timestamp of the last completion of a DB backup, successful or not.", + }) + successTime = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "db_backup_last_success_timestamp_seconds", + Help: "The timestamp of the last successful completion of a DB backup.", + }) + duration = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "db_backup_duration_seconds", + Help: "The duration of the last DB backup in seconds.", + }) + records = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "db_backup_records_processed", + Help: "The number of records processed in the last DB backup.", + }) +) + +func performBackup() (int, error) { + // Perform the backup and return the number of backed up records and any + // applicable error. + // ... + return 42, nil +} + +func ExamplePusher_Add() { + // We use a registry here to benefit from the consistency checks that + // happen during registration. + registry := prometheus.NewRegistry() + registry.MustRegister(completionTime, duration, records) + // Note that successTime is not registered. + + pusher := push.New("http://pushgateway:9091", "db_backup").Gatherer(registry) + + start := time.Now() + n, err := performBackup() + records.Set(float64(n)) + // Note that time.Since only uses a monotonic clock in Go1.9+. + duration.Set(time.Since(start).Seconds()) + completionTime.SetToCurrentTime() + if err != nil { + fmt.Println("DB backup failed:", err) + } else { + // Add successTime to pusher only in case of success. + // We could as well register it with the registry. + // This example, however, demonstrates that you can + // mix Gatherers and Collectors when handling a Pusher. + pusher.Collector(successTime) + successTime.SetToCurrentTime() + } + // Add is used here rather than Push to not delete a previously pushed + // success timestamp in case of a failure of this backup. + if err := pusher.Add(); err != nil { + fmt.Println("Could not push to Pushgateway:", err) + } +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/push/examples_test.go b/vendor/github.com/prometheus/client_golang/prometheus/push/examples_test.go index 7f17ca2913..fa5549a9ea 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/push/examples_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/push/examples_test.go @@ -15,42 +15,21 @@ package push_test import ( "fmt" - "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/push" ) -func ExampleCollectors() { +func ExamplePusher_Push() { completionTime := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "db_backup_last_completion_timestamp_seconds", - Help: "The timestamp of the last succesful completion of a DB backup.", + Help: "The timestamp of the last successful completion of a DB backup.", }) - completionTime.Set(float64(time.Now().Unix())) - if err := push.Collectors( - "db_backup", push.HostnameGroupingKey(), - "http://pushgateway:9091", - completionTime, - ); err != nil { - fmt.Println("Could not push completion time to Pushgateway:", err) - } -} - -func ExampleRegistry() { - registry := prometheus.NewRegistry() - - completionTime := prometheus.NewGauge(prometheus.GaugeOpts{ - Name: "db_backup_last_completion_timestamp_seconds", - Help: "The timestamp of the last succesful completion of a DB backup.", - }) - registry.MustRegister(completionTime) - - completionTime.Set(float64(time.Now().Unix())) - if err := push.FromGatherer( - "db_backup", push.HostnameGroupingKey(), - "http://pushgateway:9091", - registry, - ); err != nil { + completionTime.SetToCurrentTime() + if err := push.New("http://pushgateway:9091", "db_backup"). + Collector(completionTime). + Grouping("db", "customers"). + Push(); err != nil { fmt.Println("Could not push completion time to Pushgateway:", err) } } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/push/push.go b/vendor/github.com/prometheus/client_golang/prometheus/push/push.go index ae40402f8c..3721ff1988 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/push/push.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/push/push.go @@ -11,20 +11,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Copyright (c) 2013, The Prometheus Authors -// All rights reserved. +// Package push provides functions to push metrics to a Pushgateway. It uses a +// builder approach. Create a Pusher with New and then add the various options +// by using its methods, finally calling Add or Push, like this: // -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -// Package push provides functions to push metrics to a Pushgateway. The metrics -// to push are either collected from a provided registry, or from explicitly -// listed collectors. +// // Easy case: +// push.New("http://example.org/metrics", "my_job").Gatherer(myRegistry).Push() +// +// // Complex case: +// push.New("http://example.org/metrics", "my_job"). +// Collector(myCollector1). +// Collector(myCollector2). +// Grouping("zone", "xy"). +// Client(&myHTTPClient). +// BasicAuth("top", "secret"). +// Add() +// +// See the examples section for more detailed examples. // -// See the documentation of the Pushgateway to understand the meaning of the -// grouping parameters and the differences between push.Registry and -// push.Collectors on the one hand and push.AddRegistry and push.AddCollectors -// on the other hand: https://github.com/prometheus/pushgateway +// See the documentation of the Pushgateway to understand the meaning of +// the grouping key and the differences between Push and Add: +// https://github.com/prometheus/pushgateway package push import ( @@ -33,7 +40,6 @@ import ( "io/ioutil" "net/http" "net/url" - "os" "strings" "github.com/prometheus/common/expfmt" @@ -44,57 +50,149 @@ import ( const contentTypeHeader = "Content-Type" -// FromGatherer triggers a metric collection by the provided Gatherer (which is -// usually implemented by a prometheus.Registry) and pushes all gathered metrics -// to the Pushgateway specified by url, using the provided job name and the -// (optional) further grouping labels (the grouping map may be nil). See the -// Pushgateway documentation for detailed implications of the job and other -// grouping labels. Neither the job name nor any grouping label value may -// contain a "/". The metrics pushed must not contain a job label of their own -// nor any of the grouping labels. +// Pusher manages a push to the Pushgateway. Use New to create one, configure it +// with its methods, and finally use the Add or Push method to push. +type Pusher struct { + error error + + url, job string + grouping map[string]string + + gatherers prometheus.Gatherers + registerer prometheus.Registerer + + client *http.Client + useBasicAuth bool + username, password string +} + +// New creates a new Pusher to push to the provided URL with the provided job +// name. You can use just host:port or ip:port as url, in which case “http://” +// is added automatically. Alternatively, include the schema in the +// URL. However, do not include the “/metrics/jobs/…” part. // -// You can use just host:port or ip:port as url, in which case 'http://' is -// added automatically. You can also include the schema in the URL. However, do -// not include the '/metrics/jobs/...' part. +// Note that until https://github.com/prometheus/pushgateway/issues/97 is +// resolved, a “/” character in the job name is prohibited. +func New(url, job string) *Pusher { + var ( + reg = prometheus.NewRegistry() + err error + ) + if !strings.Contains(url, "://") { + url = "http://" + url + } + if strings.HasSuffix(url, "/") { + url = url[:len(url)-1] + } + if strings.Contains(job, "/") { + err = fmt.Errorf("job contains '/': %s", job) + } + + return &Pusher{ + error: err, + url: url, + job: job, + grouping: map[string]string{}, + gatherers: prometheus.Gatherers{reg}, + registerer: reg, + client: &http.Client{}, + } +} + +// Push collects/gathers all metrics from all Collectors and Gatherers added to +// this Pusher. Then, it pushes them to the Pushgateway configured while +// creating this Pusher, using the configured job name and any added grouping +// labels as grouping key. All previously pushed metrics with the same job and +// other grouping labels will be replaced with the metrics pushed by this +// call. (It uses HTTP method “PUT” to push to the Pushgateway.) // -// Note that all previously pushed metrics with the same job and other grouping -// labels will be replaced with the metrics pushed by this call. (It uses HTTP -// method 'PUT' to push to the Pushgateway.) -func FromGatherer(job string, grouping map[string]string, url string, g prometheus.Gatherer) error { - return push(job, grouping, url, g, "PUT") +// Push returns the first error encountered by any method call (including this +// one) in the lifetime of the Pusher. +func (p *Pusher) Push() error { + return p.push("PUT") } -// AddFromGatherer works like FromGatherer, but only previously pushed metrics -// with the same name (and the same job and other grouping labels) will be -// replaced. (It uses HTTP method 'POST' to push to the Pushgateway.) -func AddFromGatherer(job string, grouping map[string]string, url string, g prometheus.Gatherer) error { - return push(job, grouping, url, g, "POST") +// Add works like push, but only previously pushed metrics with the same name +// (and the same job and other grouping labels) will be replaced. (It uses HTTP +// method “POST” to push to the Pushgateway.) +func (p *Pusher) Add() error { + return p.push("POST") } -func push(job string, grouping map[string]string, pushURL string, g prometheus.Gatherer, method string) error { - if !strings.Contains(pushURL, "://") { - pushURL = "http://" + pushURL - } - if strings.HasSuffix(pushURL, "/") { - pushURL = pushURL[:len(pushURL)-1] - } +// Gatherer adds a Gatherer to the Pusher, from which metrics will be gathered +// to push them to the Pushgateway. The gathered metrics must not contain a job +// label of their own. +// +// For convenience, this method returns a pointer to the Pusher itself. +func (p *Pusher) Gatherer(g prometheus.Gatherer) *Pusher { + p.gatherers = append(p.gatherers, g) + return p +} - if strings.Contains(job, "/") { - return fmt.Errorf("job contains '/': %s", job) +// Collector adds a Collector to the Pusher, from which metrics will be +// collected to push them to the Pushgateway. The collected metrics must not +// contain a job label of their own. +// +// For convenience, this method returns a pointer to the Pusher itself. +func (p *Pusher) Collector(c prometheus.Collector) *Pusher { + if p.error == nil { + p.error = p.registerer.Register(c) } - urlComponents := []string{url.QueryEscape(job)} - for ln, lv := range grouping { - if !model.LabelNameRE.MatchString(ln) { - return fmt.Errorf("grouping label has invalid name: %s", ln) + return p +} + +// Grouping adds a label pair to the grouping key of the Pusher, replacing any +// previously added label pair with the same label name. Note that setting any +// labels in the grouping key that are already contained in the metrics to push +// will lead to an error. +// +// For convenience, this method returns a pointer to the Pusher itself. +// +// Note that until https://github.com/prometheus/pushgateway/issues/97 is +// resolved, this method does not allow a “/” character in the label value. +func (p *Pusher) Grouping(name, value string) *Pusher { + if p.error == nil { + if !model.LabelName(name).IsValid() { + p.error = fmt.Errorf("grouping label has invalid name: %s", name) + return p } - if strings.Contains(lv, "/") { - return fmt.Errorf("value of grouping label %s contains '/': %s", ln, lv) + if strings.Contains(value, "/") { + p.error = fmt.Errorf("value of grouping label %s contains '/': %s", name, value) + return p } + p.grouping[name] = value + } + return p +} + +// Client sets a custom HTTP client for the Pusher. For convenience, this method +// returns a pointer to the Pusher itself. +func (p *Pusher) Client(c *http.Client) *Pusher { + p.client = c + return p +} + +// BasicAuth configures the Pusher to use HTTP Basic Authentication with the +// provided username and password. For convenience, this method returns a +// pointer to the Pusher itself. +func (p *Pusher) BasicAuth(username, password string) *Pusher { + p.useBasicAuth = true + p.username = username + p.password = password + return p +} + +func (p *Pusher) push(method string) error { + if p.error != nil { + return p.error + } + urlComponents := []string{url.QueryEscape(p.job)} + for ln, lv := range p.grouping { urlComponents = append(urlComponents, ln, lv) } - pushURL = fmt.Sprintf("%s/metrics/job/%s", pushURL, strings.Join(urlComponents, "/")) + pushURL := fmt.Sprintf("%s/metrics/job/%s", p.url, strings.Join(urlComponents, "/")) - mfs, err := g.Gather() + mfs, err := p.gatherers.Gather() if err != nil { return err } @@ -107,7 +205,7 @@ func push(job string, grouping map[string]string, pushURL string, g prometheus.G if l.GetName() == "job" { return fmt.Errorf("pushed metric %s (%s) already contains a job label", mf.GetName(), m) } - if _, ok := grouping[l.GetName()]; ok { + if _, ok := p.grouping[l.GetName()]; ok { return fmt.Errorf( "pushed metric %s (%s) already contains grouping label %s", mf.GetName(), m, l.GetName(), @@ -121,8 +219,11 @@ func push(job string, grouping map[string]string, pushURL string, g prometheus.G if err != nil { return err } + if p.useBasicAuth { + req.SetBasicAuth(p.username, p.password) + } req.Header.Set(contentTypeHeader, string(expfmt.FmtProtoDelim)) - resp, err := http.DefaultClient.Do(req) + resp, err := p.client.Do(req) if err != nil { return err } @@ -133,40 +234,3 @@ func push(job string, grouping map[string]string, pushURL string, g prometheus.G } return nil } - -// Collectors works like FromGatherer, but it does not use a Gatherer. Instead, -// it collects from the provided collectors directly. It is a convenient way to -// push only a few metrics. -func Collectors(job string, grouping map[string]string, url string, collectors ...prometheus.Collector) error { - return pushCollectors(job, grouping, url, "PUT", collectors...) -} - -// AddCollectors works like AddFromGatherer, but it does not use a Gatherer. -// Instead, it collects from the provided collectors directly. It is a -// convenient way to push only a few metrics. -func AddCollectors(job string, grouping map[string]string, url string, collectors ...prometheus.Collector) error { - return pushCollectors(job, grouping, url, "POST", collectors...) -} - -func pushCollectors(job string, grouping map[string]string, url, method string, collectors ...prometheus.Collector) error { - r := prometheus.NewRegistry() - for _, collector := range collectors { - if err := r.Register(collector); err != nil { - return err - } - } - return push(job, grouping, url, r, method) -} - -// HostnameGroupingKey returns a label map with the only entry -// {instance=""}. This can be conveniently used as the grouping -// parameter if metrics should be pushed with the hostname as label. The -// returned map is created upon each call so that the caller is free to add more -// labels to the map. -func HostnameGroupingKey() map[string]string { - hostname, err := os.Hostname() - if err != nil { - return map[string]string{"instance": "unknown"} - } - return map[string]string{"instance": hostname} -} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/push/push_test.go b/vendor/github.com/prometheus/client_golang/prometheus/push/push_test.go index 28ed9b74b6..34ec334bb4 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/push/push_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/push/push_test.go @@ -11,12 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Copyright (c) 2013, The Prometheus Authors -// All rights reserved. -// -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - package push import ( @@ -24,7 +18,6 @@ import ( "io/ioutil" "net/http" "net/http/httptest" - "os" "testing" "github.com/prometheus/common/expfmt" @@ -40,11 +33,6 @@ func TestPush(t *testing.T) { lastPath string ) - host, err := os.Hostname() - if err != nil { - t.Error(err) - } - // Fake a Pushgateway that always responds with 202. pgwOK := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -98,26 +86,32 @@ func TestPush(t *testing.T) { } wantBody := buf.Bytes() - // PushCollectors, all good. - if err := Collectors("testjob", HostnameGroupingKey(), pgwOK.URL, metric1, metric2); err != nil { + // Push some Collectors, all good. + if err := New(pgwOK.URL, "testjob"). + Collector(metric1). + Collector(metric2). + Push(); err != nil { t.Fatal(err) } if lastMethod != "PUT" { - t.Error("want method PUT for PushCollectors, got", lastMethod) + t.Error("want method PUT for Push, got", lastMethod) } if bytes.Compare(lastBody, wantBody) != 0 { t.Errorf("got body %v, want %v", lastBody, wantBody) } - if lastPath != "/metrics/job/testjob/instance/"+host { + if lastPath != "/metrics/job/testjob" { t.Error("unexpected path:", lastPath) } - // PushAddCollectors, with nil grouping, all good. - if err := AddCollectors("testjob", nil, pgwOK.URL, metric1, metric2); err != nil { + // Add some Collectors, with nil grouping, all good. + if err := New(pgwOK.URL, "testjob"). + Collector(metric1). + Collector(metric2). + Add(); err != nil { t.Fatal(err) } if lastMethod != "POST" { - t.Error("want method POST for PushAddCollectors, got", lastMethod) + t.Error("want method POST for Add, got", lastMethod) } if bytes.Compare(lastBody, wantBody) != 0 { t.Errorf("got body %v, want %v", lastBody, wantBody) @@ -126,8 +120,11 @@ func TestPush(t *testing.T) { t.Error("unexpected path:", lastPath) } - // PushCollectors with a broken PGW. - if err := Collectors("testjob", nil, pgwErr.URL, metric1, metric2); err == nil { + // Push some Collectors with a broken PGW. + if err := New(pgwErr.URL, "testjob"). + Collector(metric1). + Collector(metric2). + Push(); err == nil { t.Error("push to broken Pushgateway succeeded") } else { if got, want := err.Error(), "unexpected status code 500 while pushing to "+pgwErr.URL+"/metrics/job/testjob: fake error\n"; got != want { @@ -135,22 +132,39 @@ func TestPush(t *testing.T) { } } - // PushCollectors with invalid grouping or job. - if err := Collectors("testjob", map[string]string{"foo": "bums"}, pgwErr.URL, metric1, metric2); err == nil { + // Push some Collectors with invalid grouping or job. + if err := New(pgwOK.URL, "testjob"). + Grouping("foo", "bums"). + Collector(metric1). + Collector(metric2). + Push(); err == nil { t.Error("push with grouping contained in metrics succeeded") } - if err := Collectors("test/job", nil, pgwErr.URL, metric1, metric2); err == nil { + if err := New(pgwOK.URL, "test/job"). + Collector(metric1). + Collector(metric2). + Push(); err == nil { t.Error("push with invalid job value succeeded") } - if err := Collectors("testjob", map[string]string{"foo/bar": "bums"}, pgwErr.URL, metric1, metric2); err == nil { + if err := New(pgwOK.URL, "testjob"). + Grouping("foobar", "bu/ms"). + Collector(metric1). + Collector(metric2). + Push(); err == nil { t.Error("push with invalid grouping succeeded") } - if err := Collectors("testjob", map[string]string{"foo-bar": "bums"}, pgwErr.URL, metric1, metric2); err == nil { + if err := New(pgwOK.URL, "testjob"). + Grouping("foo-bar", "bums"). + Collector(metric1). + Collector(metric2). + Push(); err == nil { t.Error("push with invalid grouping succeeded") } // Push registry, all good. - if err := FromGatherer("testjob", HostnameGroupingKey(), pgwOK.URL, reg); err != nil { + if err := New(pgwOK.URL, "testjob"). + Gatherer(reg). + Push(); err != nil { t.Fatal(err) } if lastMethod != "PUT" { @@ -160,12 +174,16 @@ func TestPush(t *testing.T) { t.Errorf("got body %v, want %v", lastBody, wantBody) } - // PushAdd registry, all good. - if err := AddFromGatherer("testjob", map[string]string{"a": "x", "b": "y"}, pgwOK.URL, reg); err != nil { + // Add registry, all good. + if err := New(pgwOK.URL, "testjob"). + Grouping("a", "x"). + Grouping("b", "y"). + Gatherer(reg). + Add(); err != nil { t.Fatal(err) } if lastMethod != "POST" { - t.Error("want method POSTT for PushAdd, got", lastMethod) + t.Error("want method POST for Add, got", lastMethod) } if bytes.Compare(lastBody, wantBody) != 0 { t.Errorf("got body %v, want %v", lastBody, wantBody) diff --git a/vendor/github.com/prometheus/client_golang/prometheus/registry.go b/vendor/github.com/prometheus/client_golang/prometheus/registry.go index 32a3986b06..b5e70b93fa 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/registry.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/registry.go @@ -15,15 +15,22 @@ package prometheus import ( "bytes" - "errors" "fmt" + "io/ioutil" "os" + "path/filepath" + "runtime" "sort" + "strings" "sync" + "unicode/utf8" "github.com/golang/protobuf/proto" + "github.com/prometheus/common/expfmt" dto "github.com/prometheus/client_model/go" + + "github.com/prometheus/client_golang/prometheus/internal" ) const ( @@ -35,13 +42,14 @@ const ( // DefaultRegisterer and DefaultGatherer are the implementations of the // Registerer and Gatherer interface a number of convenience functions in this // package act on. Initially, both variables point to the same Registry, which -// has a process collector (see NewProcessCollector) and a Go collector (see -// NewGoCollector) already registered. This approach to keep default instances -// as global state mirrors the approach of other packages in the Go standard -// library. Note that there are caveats. Change the variables with caution and -// only if you understand the consequences. Users who want to avoid global state -// altogether should not use the convenience function and act on custom -// instances instead. +// has a process collector (currently on Linux only, see NewProcessCollector) +// and a Go collector (see NewGoCollector, in particular the note about +// stop-the-world implication with Go versions older than 1.9) already +// registered. This approach to keep default instances as global state mirrors +// the approach of other packages in the Go standard library. Note that there +// are caveats. Change the variables with caution and only if you understand the +// consequences. Users who want to avoid global state altogether should not use +// the convenience functions and act on custom instances instead. var ( defaultRegistry = NewRegistry() DefaultRegisterer Registerer = defaultRegistry @@ -49,7 +57,7 @@ var ( ) func init() { - MustRegister(NewProcessCollector(os.Getpid(), "")) + MustRegister(NewProcessCollector(ProcessCollectorOpts{})) MustRegister(NewGoCollector()) } @@ -65,7 +73,8 @@ func NewRegistry() *Registry { // NewPedanticRegistry returns a registry that checks during collection if each // collected Metric is consistent with its reported Desc, and if the Desc has -// actually been registered with the registry. +// actually been registered with the registry. Unchecked Collectors (those whose +// Describe methed does not yield any descriptors) are excluded from the check. // // Usually, a Registry will be happy as long as the union of all collected // Metrics is consistent and valid even if some metrics are not consistent with @@ -80,7 +89,7 @@ func NewPedanticRegistry() *Registry { // Registerer is the interface for the part of a registry in charge of // registering and unregistering. Users of custom registries should use -// Registerer as type for registration purposes (rather then the Registry type +// Registerer as type for registration purposes (rather than the Registry type // directly). In that way, they are free to use custom Registerer implementation // (e.g. for testing purposes). type Registerer interface { @@ -95,8 +104,13 @@ type Registerer interface { // returned error is an instance of AlreadyRegisteredError, which // contains the previously registered Collector. // - // It is in general not safe to register the same Collector multiple - // times concurrently. + // A Collector whose Describe method does not yield any Desc is treated + // as unchecked. Registration will always succeed. No check for + // re-registering (see previous paragraph) is performed. Thus, the + // caller is responsible for not double-registering the same unchecked + // Collector, and for providing a Collector that will not cause + // inconsistent metrics on collection. (This would lead to scrape + // errors.) Register(Collector) error // MustRegister works like Register but registers any number of // Collectors and panics upon the first registration that causes an @@ -105,7 +119,9 @@ type Registerer interface { // Unregister unregisters the Collector that equals the Collector passed // in as an argument. (Two Collectors are considered equal if their // Describe method yields the same set of descriptors.) The function - // returns whether a Collector was unregistered. + // returns whether a Collector was unregistered. Note that an unchecked + // Collector cannot be unregistered (as its Describe method does not + // yield any descriptor). // // Note that even after unregistering, it will not be possible to // register a new Collector that is inconsistent with the unregistered @@ -123,15 +139,23 @@ type Registerer interface { type Gatherer interface { // Gather calls the Collect method of the registered Collectors and then // gathers the collected metrics into a lexicographically sorted slice - // of MetricFamily protobufs. Even if an error occurs, Gather attempts - // to gather as many metrics as possible. Hence, if a non-nil error is - // returned, the returned MetricFamily slice could be nil (in case of a - // fatal error that prevented any meaningful metric collection) or - // contain a number of MetricFamily protobufs, some of which might be - // incomplete, and some might be missing altogether. The returned error - // (which might be a MultiError) explains the details. In scenarios - // where complete collection is critical, the returned MetricFamily - // protobufs should be disregarded if the returned error is non-nil. + // of uniquely named MetricFamily protobufs. Gather ensures that the + // returned slice is valid and self-consistent so that it can be used + // for valid exposition. As an exception to the strict consistency + // requirements described for metric.Desc, Gather will tolerate + // different sets of label names for metrics of the same metric family. + // + // Even if an error occurs, Gather attempts to gather as many metrics as + // possible. Hence, if a non-nil error is returned, the returned + // MetricFamily slice could be nil (in case of a fatal error that + // prevented any meaningful metric collection) or contain a number of + // MetricFamily protobufs, some of which might be incomplete, and some + // might be missing altogether. The returned error (which might be a + // MultiError) explains the details. Note that this is mostly useful for + // debugging purposes. If the gathered protobufs are to be used for + // exposition in actual monitoring, it is almost always better to not + // expose an incomplete result and instead disregard the returned + // MetricFamily protobufs in case the returned error is non-nil. Gather() ([]*dto.MetricFamily, error) } @@ -152,38 +176,6 @@ func MustRegister(cs ...Collector) { DefaultRegisterer.MustRegister(cs...) } -// RegisterOrGet registers the provided Collector with the DefaultRegisterer and -// returns the Collector, unless an equal Collector was registered before, in -// which case that Collector is returned. -// -// Deprecated: RegisterOrGet is merely a convenience function for the -// implementation as described in the documentation for -// AlreadyRegisteredError. As the use case is relatively rare, this function -// will be removed in a future version of this package to clean up the -// namespace. -func RegisterOrGet(c Collector) (Collector, error) { - if err := Register(c); err != nil { - if are, ok := err.(AlreadyRegisteredError); ok { - return are.ExistingCollector, nil - } - return nil, err - } - return c, nil -} - -// MustRegisterOrGet behaves like RegisterOrGet but panics instead of returning -// an error. -// -// Deprecated: This is deprecated for the same reason RegisterOrGet is. See -// there for details. -func MustRegisterOrGet(c Collector) Collector { - c, err := RegisterOrGet(c) - if err != nil { - panic(err) - } - return c -} - // Unregister removes the registration of the provided Collector from the // DefaultRegisterer. // @@ -201,25 +193,6 @@ func (gf GathererFunc) Gather() ([]*dto.MetricFamily, error) { return gf() } -// SetMetricFamilyInjectionHook replaces the DefaultGatherer with one that -// gathers from the previous DefaultGatherers but then merges the MetricFamily -// protobufs returned from the provided hook function with the MetricFamily -// protobufs returned from the original DefaultGatherer. -// -// Deprecated: This function manipulates the DefaultGatherer variable. Consider -// the implications, i.e. don't do this concurrently with any uses of the -// DefaultGatherer. In the rare cases where you need to inject MetricFamily -// protobufs directly, it is recommended to use a custom Registry and combine it -// with a custom Gatherer using the Gatherers type (see -// there). SetMetricFamilyInjectionHook only exists for compatibility reasons -// with previous versions of this package. -func SetMetricFamilyInjectionHook(hook func() []*dto.MetricFamily) { - DefaultGatherer = Gatherers{ - DefaultGatherer, - GathererFunc(func() ([]*dto.MetricFamily, error) { return hook(), nil }), - } -} - // AlreadyRegisteredError is returned by the Register method if the Collector to // be registered has already been registered before, or a different Collector // that collects the same metrics has been registered before. Registration fails @@ -252,6 +225,13 @@ func (errs MultiError) Error() string { return buf.String() } +// Append appends the provided error if it is not nil. +func (errs *MultiError) Append(err error) { + if err != nil { + *errs = append(*errs, err) + } +} + // MaybeUnwrap returns nil if len(errs) is 0. It returns the first and only // contained error as error if len(errs is 1). In all other cases, it returns // the MultiError directly. This is helpful for returning a MultiError in a way @@ -276,6 +256,7 @@ type Registry struct { collectorsByID map[uint64]Collector // ID is a hash of the descIDs. descIDs map[uint64]struct{} dimHashesByName map[string]uint64 + uncheckedCollectors []Collector pedanticChecksEnabled bool } @@ -293,8 +274,13 @@ func (r *Registry) Register(c Collector) error { close(descChan) }() r.mtx.Lock() - defer r.mtx.Unlock() - // Coduct various tests... + defer func() { + // Drain channel in case of premature return to not leak a goroutine. + for range descChan { + } + r.mtx.Unlock() + }() + // Conduct various tests... for desc := range descChan { // Is the descriptor valid at all? @@ -333,9 +319,10 @@ func (r *Registry) Register(c Collector) error { } } } - // Did anything happen at all? + // A Collector yielding no Desc at all is considered unchecked. if len(newDescIDs) == 0 { - return errors.New("collector has no descriptors") + r.uncheckedCollectors = append(r.uncheckedCollectors, c) + return nil } if existing, exists := r.collectorsByID[collectorID]; exists { return AlreadyRegisteredError{ @@ -409,31 +396,25 @@ func (r *Registry) MustRegister(cs ...Collector) { // Gather implements Gatherer. func (r *Registry) Gather() ([]*dto.MetricFamily, error) { var ( - metricChan = make(chan Metric, capMetricChan) - metricHashes = map[uint64]struct{}{} - dimHashes = map[string]uint64{} - wg sync.WaitGroup - errs MultiError // The collected errors to return in the end. - registeredDescIDs map[uint64]struct{} // Only used for pedantic checks + checkedMetricChan = make(chan Metric, capMetricChan) + uncheckedMetricChan = make(chan Metric, capMetricChan) + metricHashes = map[uint64]struct{}{} + wg sync.WaitGroup + errs MultiError // The collected errors to return in the end. + registeredDescIDs map[uint64]struct{} // Only used for pedantic checks ) r.mtx.RLock() + goroutineBudget := len(r.collectorsByID) + len(r.uncheckedCollectors) metricFamiliesByName := make(map[string]*dto.MetricFamily, len(r.dimHashesByName)) - - // Scatter. - // (Collectors could be complex and slow, so we call them all at once.) - wg.Add(len(r.collectorsByID)) - go func() { - wg.Wait() - close(metricChan) - }() + checkedCollectors := make(chan Collector, len(r.collectorsByID)) + uncheckedCollectors := make(chan Collector, len(r.uncheckedCollectors)) for _, collector := range r.collectorsByID { - go func(collector Collector) { - defer wg.Done() - collector.Collect(metricChan) - }(collector) + checkedCollectors <- collector + } + for _, collector := range r.uncheckedCollectors { + uncheckedCollectors <- collector } - // In case pedantic checks are enabled, we have to copy the map before // giving up the RLock. if r.pedanticChecksEnabled { @@ -442,127 +423,258 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { registeredDescIDs[id] = struct{}{} } } - r.mtx.RUnlock() - // Drain metricChan in case of premature return. + wg.Add(goroutineBudget) + + collectWorker := func() { + for { + select { + case collector := <-checkedCollectors: + collector.Collect(checkedMetricChan) + case collector := <-uncheckedCollectors: + collector.Collect(uncheckedMetricChan) + default: + return + } + wg.Done() + } + } + + // Start the first worker now to make sure at least one is running. + go collectWorker() + goroutineBudget-- + + // Close checkedMetricChan and uncheckedMetricChan once all collectors + // are collected. + go func() { + wg.Wait() + close(checkedMetricChan) + close(uncheckedMetricChan) + }() + + // Drain checkedMetricChan and uncheckedMetricChan in case of premature return. defer func() { - for _ = range metricChan { + if checkedMetricChan != nil { + for range checkedMetricChan { + } + } + if uncheckedMetricChan != nil { + for range uncheckedMetricChan { + } } }() - // Gather. - for metric := range metricChan { - // This could be done concurrently, too, but it required locking - // of metricFamiliesByName (and of metricHashes if checks are - // enabled). Most likely not worth it. - desc := metric.Desc() - dtoMetric := &dto.Metric{} - if err := metric.Write(dtoMetric); err != nil { - errs = append(errs, fmt.Errorf( - "error collecting metric %v: %s", desc, err, + // Copy the channel references so we can nil them out later to remove + // them from the select statements below. + cmc := checkedMetricChan + umc := uncheckedMetricChan + + for { + select { + case metric, ok := <-cmc: + if !ok { + cmc = nil + break + } + errs.Append(processMetric( + metric, metricFamiliesByName, + metricHashes, + registeredDescIDs, )) - continue - } - metricFamily, ok := metricFamiliesByName[desc.fqName] - if ok { - if metricFamily.GetHelp() != desc.help { - errs = append(errs, fmt.Errorf( - "collected metric %s %s has help %q but should have %q", - desc.fqName, dtoMetric, desc.help, metricFamily.GetHelp(), - )) - continue + case metric, ok := <-umc: + if !ok { + umc = nil + break } - // TODO(beorn7): Simplify switch once Desc has type. - switch metricFamily.GetType() { - case dto.MetricType_COUNTER: - if dtoMetric.Counter == nil { - errs = append(errs, fmt.Errorf( - "collected metric %s %s should be a Counter", - desc.fqName, dtoMetric, - )) - continue - } - case dto.MetricType_GAUGE: - if dtoMetric.Gauge == nil { - errs = append(errs, fmt.Errorf( - "collected metric %s %s should be a Gauge", - desc.fqName, dtoMetric, - )) - continue - } - case dto.MetricType_SUMMARY: - if dtoMetric.Summary == nil { - errs = append(errs, fmt.Errorf( - "collected metric %s %s should be a Summary", - desc.fqName, dtoMetric, - )) - continue - } - case dto.MetricType_UNTYPED: - if dtoMetric.Untyped == nil { - errs = append(errs, fmt.Errorf( - "collected metric %s %s should be Untyped", - desc.fqName, dtoMetric, + errs.Append(processMetric( + metric, metricFamiliesByName, + metricHashes, + nil, + )) + default: + if goroutineBudget <= 0 || len(checkedCollectors)+len(uncheckedCollectors) == 0 { + // All collectors are already being worked on or + // we have already as many goroutines started as + // there are collectors. Do the same as above, + // just without the default. + select { + case metric, ok := <-cmc: + if !ok { + cmc = nil + break + } + errs.Append(processMetric( + metric, metricFamiliesByName, + metricHashes, + registeredDescIDs, )) - continue - } - case dto.MetricType_HISTOGRAM: - if dtoMetric.Histogram == nil { - errs = append(errs, fmt.Errorf( - "collected metric %s %s should be a Histogram", - desc.fqName, dtoMetric, + case metric, ok := <-umc: + if !ok { + umc = nil + break + } + errs.Append(processMetric( + metric, metricFamiliesByName, + metricHashes, + nil, )) - continue } - default: - panic("encountered MetricFamily with invalid type") + break } - } else { - metricFamily = &dto.MetricFamily{} - metricFamily.Name = proto.String(desc.fqName) - metricFamily.Help = proto.String(desc.help) - // TODO(beorn7): Simplify switch once Desc has type. - switch { - case dtoMetric.Gauge != nil: - metricFamily.Type = dto.MetricType_GAUGE.Enum() - case dtoMetric.Counter != nil: - metricFamily.Type = dto.MetricType_COUNTER.Enum() - case dtoMetric.Summary != nil: - metricFamily.Type = dto.MetricType_SUMMARY.Enum() - case dtoMetric.Untyped != nil: - metricFamily.Type = dto.MetricType_UNTYPED.Enum() - case dtoMetric.Histogram != nil: - metricFamily.Type = dto.MetricType_HISTOGRAM.Enum() - default: - errs = append(errs, fmt.Errorf( - "empty metric collected: %s", dtoMetric, - )) - continue + // Start more workers. + go collectWorker() + goroutineBudget-- + runtime.Gosched() + } + // Once both checkedMetricChan and uncheckdMetricChan are closed + // and drained, the contraption above will nil out cmc and umc, + // and then we can leave the collect loop here. + if cmc == nil && umc == nil { + break + } + } + return internal.NormalizeMetricFamilies(metricFamiliesByName), errs.MaybeUnwrap() +} + +// WriteToTextfile calls Gather on the provided Gatherer, encodes the result in the +// Prometheus text format, and writes it to a temporary file. Upon success, the +// temporary file is renamed to the provided filename. +// +// This is intended for use with the textfile collector of the node exporter. +// Note that the node exporter expects the filename to be suffixed with ".prom". +func WriteToTextfile(filename string, g Gatherer) error { + tmp, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)) + if err != nil { + return err + } + defer os.Remove(tmp.Name()) + + mfs, err := g.Gather() + if err != nil { + return err + } + for _, mf := range mfs { + if _, err := expfmt.MetricFamilyToText(tmp, mf); err != nil { + return err + } + } + if err := tmp.Close(); err != nil { + return err + } + + if err := os.Chmod(tmp.Name(), 0644); err != nil { + return err + } + return os.Rename(tmp.Name(), filename) +} + +// processMetric is an internal helper method only used by the Gather method. +func processMetric( + metric Metric, + metricFamiliesByName map[string]*dto.MetricFamily, + metricHashes map[uint64]struct{}, + registeredDescIDs map[uint64]struct{}, +) error { + desc := metric.Desc() + // Wrapped metrics collected by an unchecked Collector can have an + // invalid Desc. + if desc.err != nil { + return desc.err + } + dtoMetric := &dto.Metric{} + if err := metric.Write(dtoMetric); err != nil { + return fmt.Errorf("error collecting metric %v: %s", desc, err) + } + metricFamily, ok := metricFamiliesByName[desc.fqName] + if ok { // Existing name. + if metricFamily.GetHelp() != desc.help { + return fmt.Errorf( + "collected metric %s %s has help %q but should have %q", + desc.fqName, dtoMetric, desc.help, metricFamily.GetHelp(), + ) + } + // TODO(beorn7): Simplify switch once Desc has type. + switch metricFamily.GetType() { + case dto.MetricType_COUNTER: + if dtoMetric.Counter == nil { + return fmt.Errorf( + "collected metric %s %s should be a Counter", + desc.fqName, dtoMetric, + ) } - metricFamiliesByName[desc.fqName] = metricFamily - } - if err := checkMetricConsistency(metricFamily, dtoMetric, metricHashes, dimHashes); err != nil { - errs = append(errs, err) - continue - } - if r.pedanticChecksEnabled { - // Is the desc registered at all? - if _, exist := registeredDescIDs[desc.id]; !exist { - errs = append(errs, fmt.Errorf( - "collected metric %s %s with unregistered descriptor %s", - metricFamily.GetName(), dtoMetric, desc, - )) - continue + case dto.MetricType_GAUGE: + if dtoMetric.Gauge == nil { + return fmt.Errorf( + "collected metric %s %s should be a Gauge", + desc.fqName, dtoMetric, + ) } - if err := checkDescConsistency(metricFamily, dtoMetric, desc); err != nil { - errs = append(errs, err) - continue + case dto.MetricType_SUMMARY: + if dtoMetric.Summary == nil { + return fmt.Errorf( + "collected metric %s %s should be a Summary", + desc.fqName, dtoMetric, + ) } + case dto.MetricType_UNTYPED: + if dtoMetric.Untyped == nil { + return fmt.Errorf( + "collected metric %s %s should be Untyped", + desc.fqName, dtoMetric, + ) + } + case dto.MetricType_HISTOGRAM: + if dtoMetric.Histogram == nil { + return fmt.Errorf( + "collected metric %s %s should be a Histogram", + desc.fqName, dtoMetric, + ) + } + default: + panic("encountered MetricFamily with invalid type") + } + } else { // New name. + metricFamily = &dto.MetricFamily{} + metricFamily.Name = proto.String(desc.fqName) + metricFamily.Help = proto.String(desc.help) + // TODO(beorn7): Simplify switch once Desc has type. + switch { + case dtoMetric.Gauge != nil: + metricFamily.Type = dto.MetricType_GAUGE.Enum() + case dtoMetric.Counter != nil: + metricFamily.Type = dto.MetricType_COUNTER.Enum() + case dtoMetric.Summary != nil: + metricFamily.Type = dto.MetricType_SUMMARY.Enum() + case dtoMetric.Untyped != nil: + metricFamily.Type = dto.MetricType_UNTYPED.Enum() + case dtoMetric.Histogram != nil: + metricFamily.Type = dto.MetricType_HISTOGRAM.Enum() + default: + return fmt.Errorf("empty metric collected: %s", dtoMetric) + } + if err := checkSuffixCollisions(metricFamily, metricFamiliesByName); err != nil { + return err } - metricFamily.Metric = append(metricFamily.Metric, dtoMetric) + metricFamiliesByName[desc.fqName] = metricFamily } - return normalizeMetricFamilies(metricFamiliesByName), errs.MaybeUnwrap() + if err := checkMetricConsistency(metricFamily, dtoMetric, metricHashes); err != nil { + return err + } + if registeredDescIDs != nil { + // Is the desc registered at all? + if _, exist := registeredDescIDs[desc.id]; !exist { + return fmt.Errorf( + "collected metric %s %s with unregistered descriptor %s", + metricFamily.GetName(), dtoMetric, desc, + ) + } + if err := checkDescConsistency(metricFamily, dtoMetric, desc); err != nil { + return err + } + } + metricFamily.Metric = append(metricFamily.Metric, dtoMetric) + return nil } // Gatherers is a slice of Gatherer instances that implements the Gatherer @@ -588,7 +700,6 @@ func (gs Gatherers) Gather() ([]*dto.MetricFamily, error) { var ( metricFamiliesByName = map[string]*dto.MetricFamily{} metricHashes = map[uint64]struct{}{} - dimHashes = map[string]uint64{} errs MultiError // The collected errors to return in the end. ) @@ -625,10 +736,14 @@ func (gs Gatherers) Gather() ([]*dto.MetricFamily, error) { existingMF.Name = mf.Name existingMF.Help = mf.Help existingMF.Type = mf.Type + if err := checkSuffixCollisions(existingMF, metricFamiliesByName); err != nil { + errs = append(errs, err) + continue + } metricFamiliesByName[mf.GetName()] = existingMF } for _, m := range mf.Metric { - if err := checkMetricConsistency(existingMF, m, metricHashes, dimHashes); err != nil { + if err := checkMetricConsistency(existingMF, m, metricHashes); err != nil { errs = append(errs, err) continue } @@ -636,88 +751,80 @@ func (gs Gatherers) Gather() ([]*dto.MetricFamily, error) { } } } - return normalizeMetricFamilies(metricFamiliesByName), errs.MaybeUnwrap() -} - -// metricSorter is a sortable slice of *dto.Metric. -type metricSorter []*dto.Metric - -func (s metricSorter) Len() int { - return len(s) -} - -func (s metricSorter) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -func (s metricSorter) Less(i, j int) bool { - if len(s[i].Label) != len(s[j].Label) { - // This should not happen. The metrics are - // inconsistent. However, we have to deal with the fact, as - // people might use custom collectors or metric family injection - // to create inconsistent metrics. So let's simply compare the - // number of labels in this case. That will still yield - // reproducible sorting. - return len(s[i].Label) < len(s[j].Label) - } - for n, lp := range s[i].Label { - vi := lp.GetValue() - vj := s[j].Label[n].GetValue() - if vi != vj { - return vi < vj - } - } - - // We should never arrive here. Multiple metrics with the same - // label set in the same scrape will lead to undefined ingestion - // behavior. However, as above, we have to provide stable sorting - // here, even for inconsistent metrics. So sort equal metrics - // by their timestamp, with missing timestamps (implying "now") - // coming last. - if s[i].TimestampMs == nil { - return false - } - if s[j].TimestampMs == nil { - return true - } - return s[i].GetTimestampMs() < s[j].GetTimestampMs() + return internal.NormalizeMetricFamilies(metricFamiliesByName), errs.MaybeUnwrap() } -// normalizeMetricFamilies returns a MetricFamily slice whith empty -// MetricFamilies pruned and the remaining MetricFamilies sorted by name within -// the slice, with the contained Metrics sorted within each MetricFamily. -func normalizeMetricFamilies(metricFamiliesByName map[string]*dto.MetricFamily) []*dto.MetricFamily { - for _, mf := range metricFamiliesByName { - sort.Sort(metricSorter(mf.Metric)) +// checkSuffixCollisions checks for collisions with the “magic” suffixes the +// Prometheus text format and the internal metric representation of the +// Prometheus server add while flattening Summaries and Histograms. +func checkSuffixCollisions(mf *dto.MetricFamily, mfs map[string]*dto.MetricFamily) error { + var ( + newName = mf.GetName() + newType = mf.GetType() + newNameWithoutSuffix = "" + ) + switch { + case strings.HasSuffix(newName, "_count"): + newNameWithoutSuffix = newName[:len(newName)-6] + case strings.HasSuffix(newName, "_sum"): + newNameWithoutSuffix = newName[:len(newName)-4] + case strings.HasSuffix(newName, "_bucket"): + newNameWithoutSuffix = newName[:len(newName)-7] + } + if newNameWithoutSuffix != "" { + if existingMF, ok := mfs[newNameWithoutSuffix]; ok { + switch existingMF.GetType() { + case dto.MetricType_SUMMARY: + if !strings.HasSuffix(newName, "_bucket") { + return fmt.Errorf( + "collected metric named %q collides with previously collected summary named %q", + newName, newNameWithoutSuffix, + ) + } + case dto.MetricType_HISTOGRAM: + return fmt.Errorf( + "collected metric named %q collides with previously collected histogram named %q", + newName, newNameWithoutSuffix, + ) + } + } } - names := make([]string, 0, len(metricFamiliesByName)) - for name, mf := range metricFamiliesByName { - if len(mf.Metric) > 0 { - names = append(names, name) + if newType == dto.MetricType_SUMMARY || newType == dto.MetricType_HISTOGRAM { + if _, ok := mfs[newName+"_count"]; ok { + return fmt.Errorf( + "collected histogram or summary named %q collides with previously collected metric named %q", + newName, newName+"_count", + ) + } + if _, ok := mfs[newName+"_sum"]; ok { + return fmt.Errorf( + "collected histogram or summary named %q collides with previously collected metric named %q", + newName, newName+"_sum", + ) } } - sort.Strings(names) - result := make([]*dto.MetricFamily, 0, len(names)) - for _, name := range names { - result = append(result, metricFamiliesByName[name]) + if newType == dto.MetricType_HISTOGRAM { + if _, ok := mfs[newName+"_bucket"]; ok { + return fmt.Errorf( + "collected histogram named %q collides with previously collected metric named %q", + newName, newName+"_bucket", + ) + } } - return result + return nil } // checkMetricConsistency checks if the provided Metric is consistent with the -// provided MetricFamily. It also hashed the Metric labels and the MetricFamily -// name. If the resulting hash is alread in the provided metricHashes, an error -// is returned. If not, it is added to metricHashes. The provided dimHashes maps -// MetricFamily names to their dimHash (hashed sorted label names). If dimHashes -// doesn't yet contain a hash for the provided MetricFamily, it is -// added. Otherwise, an error is returned if the existing dimHashes in not equal -// the calculated dimHash. +// provided MetricFamily. It also hashes the Metric labels and the MetricFamily +// name. If the resulting hash is already in the provided metricHashes, an error +// is returned. If not, it is added to metricHashes. func checkMetricConsistency( metricFamily *dto.MetricFamily, dtoMetric *dto.Metric, metricHashes map[uint64]struct{}, - dimHashes map[string]uint64, ) error { + name := metricFamily.GetName() + // Type consistency with metric family. if metricFamily.GetType() == dto.MetricType_GAUGE && dtoMetric.Gauge == nil || metricFamily.GetType() == dto.MetricType_COUNTER && dtoMetric.Counter == nil || @@ -725,41 +832,65 @@ func checkMetricConsistency( metricFamily.GetType() == dto.MetricType_HISTOGRAM && dtoMetric.Histogram == nil || metricFamily.GetType() == dto.MetricType_UNTYPED && dtoMetric.Untyped == nil { return fmt.Errorf( - "collected metric %s %s is not a %s", - metricFamily.GetName(), dtoMetric, metricFamily.GetType(), + "collected metric %q { %s} is not a %s", + name, dtoMetric, metricFamily.GetType(), ) } - // Is the metric unique (i.e. no other metric with the same name and the same label values)? + previousLabelName := "" + for _, labelPair := range dtoMetric.GetLabel() { + labelName := labelPair.GetName() + if labelName == previousLabelName { + return fmt.Errorf( + "collected metric %q { %s} has two or more labels with the same name: %s", + name, dtoMetric, labelName, + ) + } + if !checkLabelName(labelName) { + return fmt.Errorf( + "collected metric %q { %s} has a label with an invalid name: %s", + name, dtoMetric, labelName, + ) + } + if dtoMetric.Summary != nil && labelName == quantileLabel { + return fmt.Errorf( + "collected metric %q { %s} must not have an explicit %q label", + name, dtoMetric, quantileLabel, + ) + } + if !utf8.ValidString(labelPair.GetValue()) { + return fmt.Errorf( + "collected metric %q { %s} has a label named %q whose value is not utf8: %#v", + name, dtoMetric, labelName, labelPair.GetValue()) + } + previousLabelName = labelName + } + + // Is the metric unique (i.e. no other metric with the same name and the same labels)? h := hashNew() - h = hashAdd(h, metricFamily.GetName()) + h = hashAdd(h, name) h = hashAddByte(h, separatorByte) - dh := hashNew() // Make sure label pairs are sorted. We depend on it for the consistency // check. - sort.Sort(LabelPairSorter(dtoMetric.Label)) + if !sort.IsSorted(labelPairSorter(dtoMetric.Label)) { + // We cannot sort dtoMetric.Label in place as it is immutable by contract. + copiedLabels := make([]*dto.LabelPair, len(dtoMetric.Label)) + copy(copiedLabels, dtoMetric.Label) + sort.Sort(labelPairSorter(copiedLabels)) + dtoMetric.Label = copiedLabels + } for _, lp := range dtoMetric.Label { + h = hashAdd(h, lp.GetName()) + h = hashAddByte(h, separatorByte) h = hashAdd(h, lp.GetValue()) h = hashAddByte(h, separatorByte) - dh = hashAdd(dh, lp.GetName()) - dh = hashAddByte(dh, separatorByte) } if _, exists := metricHashes[h]; exists { return fmt.Errorf( - "collected metric %s %s was collected before with the same name and label values", - metricFamily.GetName(), dtoMetric, + "collected metric %q { %s} was collected before with the same name and label values", + name, dtoMetric, ) } - if dimHash, ok := dimHashes[metricFamily.GetName()]; ok { - if dimHash != dh { - return fmt.Errorf( - "collected metric %s %s has label dimensions inconsistent with previously collected metrics in the same metric family", - metricFamily.GetName(), dtoMetric, - ) - } - } else { - dimHashes[metricFamily.GetName()] = dh - } metricHashes[h] = struct{}{} return nil } @@ -778,8 +909,8 @@ func checkDescConsistency( } // Is the desc consistent with the content of the metric? - lpsFromDesc := make([]*dto.LabelPair, 0, len(dtoMetric.Label)) - lpsFromDesc = append(lpsFromDesc, desc.constLabelPairs...) + lpsFromDesc := make([]*dto.LabelPair, len(desc.constLabelPairs), len(dtoMetric.Label)) + copy(lpsFromDesc, desc.constLabelPairs) for _, l := range desc.variableLabels { lpsFromDesc = append(lpsFromDesc, &dto.LabelPair{ Name: proto.String(l), @@ -791,7 +922,7 @@ func checkDescConsistency( metricFamily.GetName(), dtoMetric, desc, ) } - sort.Sort(LabelPairSorter(lpsFromDesc)) + sort.Sort(labelPairSorter(lpsFromDesc)) for i, lpFromDesc := range lpsFromDesc { lpFromMetric := dtoMetric.Label[i] if lpFromDesc.GetName() != lpFromMetric.GetName() || diff --git a/vendor/github.com/prometheus/client_golang/prometheus/registry_test.go b/vendor/github.com/prometheus/client_golang/prometheus/registry_test.go index 9dacb6256d..b9d9f13b0b 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/registry_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/registry_test.go @@ -21,9 +21,15 @@ package prometheus_test import ( "bytes" + "fmt" + "io/ioutil" + "math/rand" "net/http" "net/http/httptest" + "os" + "sync" "testing" + "time" dto "github.com/prometheus/client_model/go" @@ -34,7 +40,22 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" ) +// uncheckedCollector wraps a Collector but its Describe method yields no Desc. +type uncheckedCollector struct { + c prometheus.Collector +} + +func (u uncheckedCollector) Describe(_ chan<- *prometheus.Desc) {} +func (u uncheckedCollector) Collect(c chan<- prometheus.Metric) { + u.c.Collect(c) +} + func testHandler(t testing.TB) { + // TODO(beorn7): This test is a bit too "end-to-end". It tests quite a + // few moving parts that are not strongly coupled. They could/should be + // tested separately. However, the changes planned for v0.10 will + // require a major rework of this test anyway, at which time I will + // structure it in a better way. metricVec := prometheus.NewCounterVec( prometheus.CounterOpts{ @@ -209,6 +230,117 @@ metric: < expectedMetricFamilyMergedWithExternalAsProtoCompactText := []byte(`name:"name" help:"docstring" type:COUNTER metric: label: counter: > metric: label: counter: > metric: label: counter: > `) + externalMetricFamilyWithInvalidLabelValue := &dto.MetricFamily{ + Name: proto.String("name"), + Help: proto.String("docstring"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + { + Name: proto.String("constname"), + Value: proto.String("\xFF"), + }, + { + Name: proto.String("labelname"), + Value: proto.String("different_val"), + }, + }, + Counter: &dto.Counter{ + Value: proto.Float64(42), + }, + }, + }, + } + + expectedMetricFamilyInvalidLabelValueAsText := []byte(`An error has occurred while serving metrics: + +collected metric "name" { label: label: counter: } has a label named "constname" whose value is not utf8: "\xff" +`) + + summary := prometheus.NewSummary(prometheus.SummaryOpts{ + Name: "complex", + Help: "A metric to check collisions with _sum and _count.", + }) + summaryAsText := []byte(`# HELP complex A metric to check collisions with _sum and _count. +# TYPE complex summary +complex{quantile="0.5"} NaN +complex{quantile="0.9"} NaN +complex{quantile="0.99"} NaN +complex_sum 0 +complex_count 0 +`) + histogram := prometheus.NewHistogram(prometheus.HistogramOpts{ + Name: "complex", + Help: "A metric to check collisions with _sun, _count, and _bucket.", + }) + externalMetricFamilyWithBucketSuffix := &dto.MetricFamily{ + Name: proto.String("complex_bucket"), + Help: proto.String("externaldocstring"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + { + Counter: &dto.Counter{ + Value: proto.Float64(1), + }, + }, + }, + } + externalMetricFamilyWithBucketSuffixAsText := []byte(`# HELP complex_bucket externaldocstring +# TYPE complex_bucket counter +complex_bucket 1 +`) + externalMetricFamilyWithCountSuffix := &dto.MetricFamily{ + Name: proto.String("complex_count"), + Help: proto.String("externaldocstring"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + { + Counter: &dto.Counter{ + Value: proto.Float64(1), + }, + }, + }, + } + bucketCollisionMsg := []byte(`An error has occurred while serving metrics: + +collected metric named "complex_bucket" collides with previously collected histogram named "complex" +`) + summaryCountCollisionMsg := []byte(`An error has occurred while serving metrics: + +collected metric named "complex_count" collides with previously collected summary named "complex" +`) + histogramCountCollisionMsg := []byte(`An error has occurred while serving metrics: + +collected metric named "complex_count" collides with previously collected histogram named "complex" +`) + externalMetricFamilyWithDuplicateLabel := &dto.MetricFamily{ + Name: proto.String("broken_metric"), + Help: proto.String("The registry should detect the duplicate label."), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + { + Name: proto.String("foo"), + Value: proto.String("bar"), + }, + { + Name: proto.String("foo"), + Value: proto.String("baz"), + }, + }, + Counter: &dto.Counter{ + Value: proto.Float64(2.7), + }, + }, + }, + } + duplicateLabelMsg := []byte(`An error has occurred while serving metrics: + +collected metric "broken_metric" { label: label: counter: } has two or more labels with the same name: foo +`) + type output struct { headers map[string]string body []byte @@ -226,7 +358,7 @@ metric: < }, out: output{ headers: map[string]string{ - "Content-Type": `text/plain; version=0.0.4`, + "Content-Type": `text/plain; version=0.0.4; charset=utf-8`, }, body: []byte{}, }, @@ -237,7 +369,7 @@ metric: < }, out: output{ headers: map[string]string{ - "Content-Type": `text/plain; version=0.0.4`, + "Content-Type": `text/plain; version=0.0.4; charset=utf-8`, }, body: []byte{}, }, @@ -248,7 +380,7 @@ metric: < }, out: output{ headers: map[string]string{ - "Content-Type": `text/plain; version=0.0.4`, + "Content-Type": `text/plain; version=0.0.4; charset=utf-8`, }, body: []byte{}, }, @@ -270,7 +402,7 @@ metric: < }, out: output{ headers: map[string]string{ - "Content-Type": `text/plain; version=0.0.4`, + "Content-Type": `text/plain; version=0.0.4; charset=utf-8`, }, body: expectedMetricFamilyAsText, }, @@ -294,7 +426,7 @@ metric: < }, out: output{ headers: map[string]string{ - "Content-Type": `text/plain; version=0.0.4`, + "Content-Type": `text/plain; version=0.0.4; charset=utf-8`, }, body: externalMetricFamilyAsText, }, @@ -337,7 +469,7 @@ metric: < }, out: output{ headers: map[string]string{ - "Content-Type": `text/plain; version=0.0.4`, + "Content-Type": `text/plain; version=0.0.4; charset=utf-8`, }, body: []byte{}, }, @@ -348,7 +480,7 @@ metric: < }, out: output{ headers: map[string]string{ - "Content-Type": `text/plain; version=0.0.4`, + "Content-Type": `text/plain; version=0.0.4; charset=utf-8`, }, body: expectedMetricFamilyAsText, }, @@ -360,7 +492,7 @@ metric: < }, out: output{ headers: map[string]string{ - "Content-Type": `text/plain; version=0.0.4`, + "Content-Type": `text/plain; version=0.0.4; charset=utf-8`, }, body: bytes.Join( [][]byte{ @@ -452,6 +584,114 @@ metric: < externalMetricFamilyWithSameName, }, }, + { // 16 + headers: map[string]string{ + "Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=compact-text", + }, + out: output{ + headers: map[string]string{ + "Content-Type": `text/plain; charset=utf-8`, + }, + body: expectedMetricFamilyInvalidLabelValueAsText, + }, + collector: metricVec, + externalMF: []*dto.MetricFamily{ + externalMetricFamily, + externalMetricFamilyWithInvalidLabelValue, + }, + }, + { // 17 + headers: map[string]string{ + "Accept": "text/plain", + }, + out: output{ + headers: map[string]string{ + "Content-Type": `text/plain; version=0.0.4; charset=utf-8`, + }, + body: expectedMetricFamilyAsText, + }, + collector: uncheckedCollector{metricVec}, + }, + { // 18 + headers: map[string]string{ + "Accept": "text/plain", + }, + out: output{ + headers: map[string]string{ + "Content-Type": `text/plain; charset=utf-8`, + }, + body: histogramCountCollisionMsg, + }, + collector: histogram, + externalMF: []*dto.MetricFamily{ + externalMetricFamilyWithCountSuffix, + }, + }, + { // 19 + headers: map[string]string{ + "Accept": "text/plain", + }, + out: output{ + headers: map[string]string{ + "Content-Type": `text/plain; charset=utf-8`, + }, + body: bucketCollisionMsg, + }, + collector: histogram, + externalMF: []*dto.MetricFamily{ + externalMetricFamilyWithBucketSuffix, + }, + }, + { // 20 + headers: map[string]string{ + "Accept": "text/plain", + }, + out: output{ + headers: map[string]string{ + "Content-Type": `text/plain; charset=utf-8`, + }, + body: summaryCountCollisionMsg, + }, + collector: summary, + externalMF: []*dto.MetricFamily{ + externalMetricFamilyWithCountSuffix, + }, + }, + { // 21 + headers: map[string]string{ + "Accept": "text/plain", + }, + out: output{ + headers: map[string]string{ + "Content-Type": `text/plain; version=0.0.4; charset=utf-8`, + }, + body: bytes.Join( + [][]byte{ + summaryAsText, + externalMetricFamilyWithBucketSuffixAsText, + }, + []byte{}, + ), + }, + collector: summary, + externalMF: []*dto.MetricFamily{ + externalMetricFamilyWithBucketSuffix, + }, + }, + { // 22 + headers: map[string]string{ + "Accept": "text/plain", + }, + out: output{ + headers: map[string]string{ + "Content-Type": `text/plain; charset=utf-8`, + }, + body: duplicateLabelMsg, + }, + externalMF: []*dto.MetricFamily{ + externalMetricFamilyWithDuplicateLabel, + }, + }, } for i, scenario := range scenarios { registry := prometheus.NewPedanticRegistry() @@ -466,7 +706,7 @@ metric: < } if scenario.collector != nil { - registry.Register(scenario.collector) + registry.MustRegister(scenario.collector) } writer := httptest.NewRecorder() handler := prometheus.InstrumentHandler("prometheus", promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{})) @@ -477,7 +717,7 @@ metric: < handler(writer, request) for key, value := range scenario.out.headers { - if writer.HeaderMap.Get(key) != value { + if writer.Header().Get(key) != value { t.Errorf( "%d. expected %q for header %q, got %q", i, value, key, writer.Header().Get(key), @@ -504,14 +744,8 @@ func BenchmarkHandler(b *testing.B) { } } -func TestRegisterWithOrGet(t *testing.T) { - // Replace the default registerer just to be sure. This is bad, but this - // whole test will go away once RegisterOrGet is removed. - oldRegisterer := prometheus.DefaultRegisterer - defer func() { - prometheus.DefaultRegisterer = oldRegisterer - }() - prometheus.DefaultRegisterer = prometheus.NewRegistry() +func TestAlreadyRegistered(t *testing.T) { + reg := prometheus.NewRegistry() original := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "test", @@ -526,20 +760,221 @@ func TestRegisterWithOrGet(t *testing.T) { }, []string{"foo", "bar"}, ) - if err := prometheus.Register(original); err != nil { + var err error + if err = reg.Register(original); err != nil { t.Fatal(err) } - if err := prometheus.Register(equalButNotSame); err == nil { - t.Fatal("expected error when registringe equal collector") + if err = reg.Register(equalButNotSame); err == nil { + t.Fatal("expected error when registering equal collector") + } + if are, ok := err.(prometheus.AlreadyRegisteredError); ok { + if are.ExistingCollector != original { + t.Error("expected original collector but got something else") + } + if are.ExistingCollector == equalButNotSame { + t.Error("expected original callector but got new one") + } + } else { + t.Error("unexpected error:", err) + } +} + +// TestHistogramVecRegisterGatherConcurrency is an end-to-end test that +// concurrently calls Observe on random elements of a HistogramVec while the +// same HistogramVec is registered concurrently and the Gather method of the +// registry is called concurrently. +func TestHistogramVecRegisterGatherConcurrency(t *testing.T) { + labelNames := make([]string, 16) // Need at least 13 to expose #512. + for i := range labelNames { + labelNames[i] = fmt.Sprint("label_", i) + } + + var ( + reg = prometheus.NewPedanticRegistry() + hv = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "test_histogram", + Help: "This helps testing.", + ConstLabels: prometheus.Labels{"foo": "bar"}, + }, + labelNames, + ) + labelValues = []string{"a", "b", "c", "alpha", "beta", "gamma", "aleph", "beth", "gimel"} + quit = make(chan struct{}) + wg sync.WaitGroup + ) + + observe := func() { + defer wg.Done() + for { + select { + case <-quit: + return + default: + obs := rand.NormFloat64()*.1 + .2 + values := make([]string, 0, len(labelNames)) + for range labelNames { + values = append(values, labelValues[rand.Intn(len(labelValues))]) + } + hv.WithLabelValues(values...).Observe(obs) + } + } } - existing, err := prometheus.RegisterOrGet(equalButNotSame) + + register := func() { + defer wg.Done() + for { + select { + case <-quit: + return + default: + if err := reg.Register(hv); err != nil { + if _, ok := err.(prometheus.AlreadyRegisteredError); !ok { + t.Error("Registering failed:", err) + } + } + time.Sleep(7 * time.Millisecond) + } + } + } + + gather := func() { + defer wg.Done() + for { + select { + case <-quit: + return + default: + if g, err := reg.Gather(); err != nil { + t.Error("Gathering failed:", err) + } else { + if len(g) == 0 { + continue + } + if len(g) != 1 { + t.Error("Gathered unexpected number of metric families:", len(g)) + } + if len(g[0].Metric[0].Label) != len(labelNames)+1 { + t.Error("Gathered unexpected number of label pairs:", len(g[0].Metric[0].Label)) + } + } + time.Sleep(4 * time.Millisecond) + } + } + } + + wg.Add(10) + go observe() + go observe() + go register() + go observe() + go gather() + go observe() + go register() + go observe() + go gather() + go observe() + + time.Sleep(time.Second) + close(quit) + wg.Wait() +} + +func TestWriteToTextfile(t *testing.T) { + expectedOut := `# HELP test_counter test counter +# TYPE test_counter counter +test_counter{name="qux"} 1 +# HELP test_gauge test gauge +# TYPE test_gauge gauge +test_gauge{name="baz"} 1.1 +# HELP test_hist test histogram +# TYPE test_hist histogram +test_hist_bucket{name="bar",le="0.005"} 0 +test_hist_bucket{name="bar",le="0.01"} 0 +test_hist_bucket{name="bar",le="0.025"} 0 +test_hist_bucket{name="bar",le="0.05"} 0 +test_hist_bucket{name="bar",le="0.1"} 0 +test_hist_bucket{name="bar",le="0.25"} 0 +test_hist_bucket{name="bar",le="0.5"} 0 +test_hist_bucket{name="bar",le="1"} 1 +test_hist_bucket{name="bar",le="2.5"} 1 +test_hist_bucket{name="bar",le="5"} 2 +test_hist_bucket{name="bar",le="10"} 2 +test_hist_bucket{name="bar",le="+Inf"} 2 +test_hist_sum{name="bar"} 3.64 +test_hist_count{name="bar"} 2 +# HELP test_summary test summary +# TYPE test_summary summary +test_summary{name="foo",quantile="0.5"} 10 +test_summary{name="foo",quantile="0.9"} 20 +test_summary{name="foo",quantile="0.99"} 20 +test_summary_sum{name="foo"} 30 +test_summary_count{name="foo"} 2 +` + + registry := prometheus.NewRegistry() + + summary := prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: "test_summary", + Help: "test summary", + }, + []string{"name"}, + ) + + histogram := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "test_hist", + Help: "test histogram", + }, + []string{"name"}, + ) + + gauge := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "test_gauge", + Help: "test gauge", + }, + []string{"name"}, + ) + + counter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "test_counter", + Help: "test counter", + }, + []string{"name"}, + ) + + registry.MustRegister(summary) + registry.MustRegister(histogram) + registry.MustRegister(gauge) + registry.MustRegister(counter) + + summary.With(prometheus.Labels{"name": "foo"}).Observe(10) + summary.With(prometheus.Labels{"name": "foo"}).Observe(20) + histogram.With(prometheus.Labels{"name": "bar"}).Observe(0.93) + histogram.With(prometheus.Labels{"name": "bar"}).Observe(2.71) + gauge.With(prometheus.Labels{"name": "baz"}).Set(1.1) + counter.With(prometheus.Labels{"name": "qux"}).Inc() + + tmpfile, err := ioutil.TempFile("", "prom_registry_test") if err != nil { t.Fatal(err) } - if existing != original { - t.Error("expected original collector but got something else") + defer os.Remove(tmpfile.Name()) + + if err := prometheus.WriteToTextfile(tmpfile.Name(), registry); err != nil { + t.Fatal(err) + } + + fileBytes, err := ioutil.ReadFile(tmpfile.Name()) + if err != nil { + t.Fatal(err) } - if existing == equalButNotSame { - t.Error("expected original callector but got new one") + fileContents := string(fileBytes) + + if fileContents != expectedOut { + t.Error("file contents didn't match unexpected") } } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/summary.go b/vendor/github.com/prometheus/client_golang/prometheus/summary.go index bce05bf9a0..2980614dff 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/summary.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/summary.go @@ -36,7 +36,10 @@ const quantileLabel = "quantile" // // A typical use-case is the observation of request latencies. By default, a // Summary provides the median, the 90th and the 99th percentile of the latency -// as rank estimations. +// as rank estimations. However, the default behavior will change in the +// upcoming v0.10 of the library. There will be no rank estimations at all by +// default. For a sane transition, it is recommended to set the desired rank +// estimations explicitly. // // Note that the rank estimations cannot be aggregated in a meaningful way with // the Prometheus query language (i.e. you cannot average or add them). If you @@ -54,6 +57,9 @@ type Summary interface { } // DefObjectives are the default Summary quantile values. +// +// Deprecated: DefObjectives will not be used as the default objectives in +// v0.10 of the library. The default Summary will have no quantiles then. var ( DefObjectives = map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001} @@ -75,8 +81,10 @@ const ( ) // SummaryOpts bundles the options for creating a Summary metric. It is -// mandatory to set Name and Help to a non-empty string. All other fields are -// optional and can safely be left at their zero value. +// mandatory to set Name to a non-empty string. While all other fields are +// optional and can safely be left at their zero value, it is recommended to set +// a help string and to explicitly set the Objectives field to the desired value +// as the default value will change in the upcoming v0.10 of the library. type SummaryOpts struct { // Namespace, Subsystem, and Name are components of the fully-qualified // name of the Summary (created by joining these components with @@ -87,35 +95,39 @@ type SummaryOpts struct { Subsystem string Name string - // Help provides information about this Summary. Mandatory! + // Help provides information about this Summary. // // Metrics with the same fully-qualified name must have the same Help // string. Help string - // ConstLabels are used to attach fixed labels to this - // Summary. Summaries with the same fully-qualified name must have the - // same label names in their ConstLabels. + // ConstLabels are used to attach fixed labels to this metric. Metrics + // with the same fully-qualified name must have the same label names in + // their ConstLabels. // - // Note that in most cases, labels have a value that varies during the - // lifetime of a process. Those labels are usually managed with a - // SummaryVec. ConstLabels serve only special purposes. One is for the - // special case where the value of a label does not change during the - // lifetime of a process, e.g. if the revision of the running binary is - // put into a label. Another, more advanced purpose is if more than one - // Collector needs to collect Summaries with the same fully-qualified - // name. In that case, those Summaries must differ in the values of - // their ConstLabels. See the Collector examples. + // Due to the way a Summary is represented in the Prometheus text format + // and how it is handled by the Prometheus server internally, “quantile” + // is an illegal label name. Construction of a Summary or SummaryVec + // will panic if this label name is used in ConstLabels. // - // If the value of a label never changes (not even between binaries), - // that label most likely should not be a label at all (but part of the - // metric name). + // ConstLabels are only used rarely. In particular, do not use them to + // attach the same labels to all your metrics. Those use cases are + // better covered by target labels set by the scraping Prometheus + // server, or by one specific metric (e.g. a build_info or a + // machine_role metric). See also + // https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels,-not-static-scraped-labels ConstLabels Labels // Objectives defines the quantile rank estimates with their respective - // absolute error. If Objectives[q] = e, then the value reported - // for q will be the φ-quantile value for some φ between q-e and q+e. - // The default value is DefObjectives. + // absolute error. If Objectives[q] = e, then the value reported for q + // will be the φ-quantile value for some φ between q-e and q+e. The + // default value is DefObjectives. It is used if Objectives is left at + // its zero value (i.e. nil). To create a Summary without Objectives, + // set it to an empty map (i.e. map[float64]float64{}). + // + // Deprecated: Note that the current value of DefObjectives is + // deprecated. It will be replaced by an empty map in v0.10 of the + // library. Please explicitly set Objectives to the desired value. Objectives map[float64]float64 // MaxAge defines the duration for which an observation stays relevant @@ -169,7 +181,7 @@ func NewSummary(opts SummaryOpts) Summary { func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary { if len(desc.variableLabels) != len(labelValues) { - panic(errInconsistentCardinality) + panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, labelValues)) } for _, n := range desc.variableLabels { @@ -183,7 +195,7 @@ func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary { } } - if len(opts.Objectives) == 0 { + if opts.Objectives == nil { opts.Objectives = DefObjectives } @@ -390,13 +402,21 @@ func (s quantSort) Less(i, j int) bool { // (e.g. HTTP request latencies, partitioned by status code and method). Create // instances with NewSummaryVec. type SummaryVec struct { - *MetricVec + *metricVec } // NewSummaryVec creates a new SummaryVec based on the provided SummaryOpts and -// partitioned by the given label names. At least one label name must be -// provided. +// partitioned by the given label names. +// +// Due to the way a Summary is represented in the Prometheus text format and how +// it is handled by the Prometheus server internally, “quantile” is an illegal +// label name. NewSummaryVec will panic if this label name is used. func NewSummaryVec(opts SummaryOpts, labelNames []string) *SummaryVec { + for _, ln := range labelNames { + if ln == quantileLabel { + panic(errQuantileLabelNotAllowed) + } + } desc := NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), opts.Help, @@ -404,47 +424,116 @@ func NewSummaryVec(opts SummaryOpts, labelNames []string) *SummaryVec { opts.ConstLabels, ) return &SummaryVec{ - MetricVec: newMetricVec(desc, func(lvs ...string) Metric { + metricVec: newMetricVec(desc, func(lvs ...string) Metric { return newSummary(desc, opts, lvs...) }), } } -// GetMetricWithLabelValues replaces the method of the same name in -// MetricVec. The difference is that this method returns a Summary and not a -// Metric so that no type conversion is required. -func (m *SummaryVec) GetMetricWithLabelValues(lvs ...string) (Summary, error) { - metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...) +// GetMetricWithLabelValues returns the Summary for the given slice of label +// values (same order as the VariableLabels in Desc). If that combination of +// label values is accessed for the first time, a new Summary is created. +// +// It is possible to call this method without using the returned Summary to only +// create the new Summary but leave it at its starting value, a Summary without +// any observations. +// +// Keeping the Summary for later use is possible (and should be considered if +// performance is critical), but keep in mind that Reset, DeleteLabelValues and +// Delete can be used to delete the Summary from the SummaryVec. In that case, +// the Summary will still exist, but it will not be exported anymore, even if a +// Summary with the same label values is created later. See also the CounterVec +// example. +// +// An error is returned if the number of label values is not the same as the +// number of VariableLabels in Desc (minus any curried labels). +// +// Note that for more than one label value, this method is prone to mistakes +// caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as +// an alternative to avoid that type of mistake. For higher label numbers, the +// latter has a much more readable (albeit more verbose) syntax, but it comes +// with a performance overhead (for creating and processing the Labels map). +// See also the GaugeVec example. +func (v *SummaryVec) GetMetricWithLabelValues(lvs ...string) (Observer, error) { + metric, err := v.metricVec.getMetricWithLabelValues(lvs...) if metric != nil { - return metric.(Summary), err + return metric.(Observer), err } return nil, err } -// GetMetricWith replaces the method of the same name in MetricVec. The -// difference is that this method returns a Summary and not a Metric so that no -// type conversion is required. -func (m *SummaryVec) GetMetricWith(labels Labels) (Summary, error) { - metric, err := m.MetricVec.GetMetricWith(labels) +// GetMetricWith returns the Summary for the given Labels map (the label names +// must match those of the VariableLabels in Desc). If that label map is +// accessed for the first time, a new Summary is created. Implications of +// creating a Summary without using it and keeping the Summary for later use are +// the same as for GetMetricWithLabelValues. +// +// An error is returned if the number and names of the Labels are inconsistent +// with those of the VariableLabels in Desc (minus any curried labels). +// +// This method is used for the same purpose as +// GetMetricWithLabelValues(...string). See there for pros and cons of the two +// methods. +func (v *SummaryVec) GetMetricWith(labels Labels) (Observer, error) { + metric, err := v.metricVec.getMetricWith(labels) if metric != nil { - return metric.(Summary), err + return metric.(Observer), err } return nil, err } // WithLabelValues works as GetMetricWithLabelValues, but panics where -// GetMetricWithLabelValues would have returned an error. By not returning an -// error, WithLabelValues allows shortcuts like +// GetMetricWithLabelValues would have returned an error. Not returning an +// error allows shortcuts like // myVec.WithLabelValues("404", "GET").Observe(42.21) -func (m *SummaryVec) WithLabelValues(lvs ...string) Summary { - return m.MetricVec.WithLabelValues(lvs...).(Summary) +func (v *SummaryVec) WithLabelValues(lvs ...string) Observer { + s, err := v.GetMetricWithLabelValues(lvs...) + if err != nil { + panic(err) + } + return s } // With works as GetMetricWith, but panics where GetMetricWithLabels would have -// returned an error. By not returning an error, With allows shortcuts like -// myVec.With(Labels{"code": "404", "method": "GET"}).Observe(42.21) -func (m *SummaryVec) With(labels Labels) Summary { - return m.MetricVec.With(labels).(Summary) +// returned an error. Not returning an error allows shortcuts like +// myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Observe(42.21) +func (v *SummaryVec) With(labels Labels) Observer { + s, err := v.GetMetricWith(labels) + if err != nil { + panic(err) + } + return s +} + +// CurryWith returns a vector curried with the provided labels, i.e. the +// returned vector has those labels pre-set for all labeled operations performed +// on it. The cardinality of the curried vector is reduced accordingly. The +// order of the remaining labels stays the same (just with the curried labels +// taken out of the sequence – which is relevant for the +// (GetMetric)WithLabelValues methods). It is possible to curry a curried +// vector, but only with labels not yet used for currying before. +// +// The metrics contained in the SummaryVec are shared between the curried and +// uncurried vectors. They are just accessed differently. Curried and uncurried +// vectors behave identically in terms of collection. Only one must be +// registered with a given registry (usually the uncurried version). The Reset +// method deletes all metrics, even if called on a curried vector. +func (v *SummaryVec) CurryWith(labels Labels) (ObserverVec, error) { + vec, err := v.curryWith(labels) + if vec != nil { + return &SummaryVec{vec}, err + } + return nil, err +} + +// MustCurryWith works as CurryWith but panics where CurryWith would have +// returned an error. +func (v *SummaryVec) MustCurryWith(labels Labels) ObserverVec { + vec, err := v.CurryWith(labels) + if err != nil { + panic(err) + } + return vec } type constSummary struct { @@ -497,7 +586,7 @@ func (s *constSummary) Write(out *dto.Metric) error { // map[float64]float64{0.5: 0.23, 0.99: 0.56} // // NewConstSummary returns an error if the length of labelValues is not -// consistent with the variable labels in Desc. +// consistent with the variable labels in Desc or if Desc is invalid. func NewConstSummary( desc *Desc, count uint64, @@ -505,8 +594,11 @@ func NewConstSummary( quantiles map[float64]float64, labelValues ...string, ) (Metric, error) { - if len(desc.variableLabels) != len(labelValues) { - return nil, errInconsistentCardinality + if desc.err != nil { + return nil, desc.err + } + if err := validateLabelValues(labelValues, len(desc.variableLabels)); err != nil { + return nil, err } return &constSummary{ desc: desc, diff --git a/vendor/github.com/prometheus/client_golang/prometheus/summary_test.go b/vendor/github.com/prometheus/client_golang/prometheus/summary_test.go index c4575ffbd2..8b1a62eeee 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/summary_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/summary_test.go @@ -25,6 +25,70 @@ import ( dto "github.com/prometheus/client_model/go" ) +func TestSummaryWithDefaultObjectives(t *testing.T) { + reg := NewRegistry() + summaryWithDefaultObjectives := NewSummary(SummaryOpts{ + Name: "default_objectives", + Help: "Test help.", + }) + if err := reg.Register(summaryWithDefaultObjectives); err != nil { + t.Error(err) + } + + m := &dto.Metric{} + if err := summaryWithDefaultObjectives.Write(m); err != nil { + t.Error(err) + } + if len(m.GetSummary().Quantile) != len(DefObjectives) { + t.Error("expected default objectives in summary") + } +} + +func TestSummaryWithoutObjectives(t *testing.T) { + reg := NewRegistry() + summaryWithEmptyObjectives := NewSummary(SummaryOpts{ + Name: "empty_objectives", + Help: "Test help.", + Objectives: map[float64]float64{}, + }) + if err := reg.Register(summaryWithEmptyObjectives); err != nil { + t.Error(err) + } + + m := &dto.Metric{} + if err := summaryWithEmptyObjectives.Write(m); err != nil { + t.Error(err) + } + if len(m.GetSummary().Quantile) != 0 { + t.Error("expected no objectives in summary") + } +} + +func TestSummaryWithQuantileLabel(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Attempt to create Summary with 'quantile' label did not panic.") + } + }() + _ = NewSummary(SummaryOpts{ + Name: "test_summary", + Help: "less", + ConstLabels: Labels{"quantile": "test"}, + }) +} + +func TestSummaryVecWithQuantileLabel(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Attempt to create SummaryVec with 'quantile' label did not panic.") + } + }() + _ = NewSummaryVec(SummaryOpts{ + Name: "test_summary", + Help: "less", + }, []string{"quantile"}) +} + func benchmarkSummaryObserve(w int, b *testing.B) { b.StopTimer() @@ -136,8 +200,9 @@ func TestSummaryConcurrency(t *testing.T) { end.Add(concLevel) sum := NewSummary(SummaryOpts{ - Name: "test_summary", - Help: "helpless", + Name: "test_summary", + Help: "helpless", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }) allVars := make([]float64, total) @@ -223,8 +288,9 @@ func TestSummaryVecConcurrency(t *testing.T) { sum := NewSummaryVec( SummaryOpts{ - Name: "test_summary", - Help: "helpless", + Name: "test_summary", + Help: "helpless", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }, []string{"label"}, ) @@ -260,7 +326,7 @@ func TestSummaryVecConcurrency(t *testing.T) { for i := 0; i < vecLength; i++ { m := &dto.Metric{} s := sum.WithLabelValues(string('A' + i)) - s.Write(m) + s.(Summary).Write(m) if got, want := int(*m.Summary.SampleCount), len(allVars[i]); got != want { t.Errorf("got sample count %d for label %c, want %d", got, 'A'+i, want) } @@ -305,7 +371,7 @@ func TestSummaryDecay(t *testing.T) { m := &dto.Metric{} i := 0 tick := time.NewTicker(time.Millisecond) - for _ = range tick.C { + for range tick.C { i++ sum.Observe(float64(i)) if i%10 == 0 { diff --git a/vendor/github.com/prometheus/client_golang/prometheus/testutil/testutil.go b/vendor/github.com/prometheus/client_golang/prometheus/testutil/testutil.go new file mode 100644 index 0000000000..e344cc853c --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/testutil/testutil.go @@ -0,0 +1,189 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package testutil provides helpers to test code using the prometheus package +// of client_golang. +// +// While writing unit tests to verify correct instrumentation of your code, it's +// a common mistake to mostly test the instrumentation library instead of your +// own code. Rather than verifying that a prometheus.Counter's value has changed +// as expected or that it shows up in the exposition after registration, it is +// in general more robust and more faithful to the concept of unit tests to use +// mock implementations of the prometheus.Counter and prometheus.Registerer +// interfaces that simply assert that the Add or Register methods have been +// called with the expected arguments. However, this might be overkill in simple +// scenarios. The ToFloat64 function is provided for simple inspection of a +// single-value metric, but it has to be used with caution. +// +// End-to-end tests to verify all or larger parts of the metrics exposition can +// be implemented with the CollectAndCompare or GatherAndCompare functions. The +// most appropriate use is not so much testing instrumentation of your code, but +// testing custom prometheus.Collector implementations and in particular whole +// exporters, i.e. programs that retrieve telemetry data from a 3rd party source +// and convert it into Prometheus metrics. +package testutil + +import ( + "bytes" + "fmt" + "io" + + "github.com/prometheus/common/expfmt" + + dto "github.com/prometheus/client_model/go" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/internal" +) + +// ToFloat64 collects all Metrics from the provided Collector. It expects that +// this results in exactly one Metric being collected, which must be a Gauge, +// Counter, or Untyped. In all other cases, ToFloat64 panics. ToFloat64 returns +// the value of the collected Metric. +// +// The Collector provided is typically a simple instance of Gauge or Counter, or +// – less commonly – a GaugeVec or CounterVec with exactly one element. But any +// Collector fulfilling the prerequisites described above will do. +// +// Use this function with caution. It is computationally very expensive and thus +// not suited at all to read values from Metrics in regular code. This is really +// only for testing purposes, and even for testing, other approaches are often +// more appropriate (see this package's documentation). +// +// A clear anti-pattern would be to use a metric type from the prometheus +// package to track values that are also needed for something else than the +// exposition of Prometheus metrics. For example, you would like to track the +// number of items in a queue because your code should reject queuing further +// items if a certain limit is reached. It is tempting to track the number of +// items in a prometheus.Gauge, as it is then easily available as a metric for +// exposition, too. However, then you would need to call ToFloat64 in your +// regular code, potentially quite often. The recommended way is to track the +// number of items conventionally (in the way you would have done it without +// considering Prometheus metrics) and then expose the number with a +// prometheus.GaugeFunc. +func ToFloat64(c prometheus.Collector) float64 { + var ( + m prometheus.Metric + mCount int + mChan = make(chan prometheus.Metric) + done = make(chan struct{}) + ) + + go func() { + for m = range mChan { + mCount++ + } + close(done) + }() + + c.Collect(mChan) + close(mChan) + <-done + + if mCount != 1 { + panic(fmt.Errorf("collected %d metrics instead of exactly 1", mCount)) + } + + pb := &dto.Metric{} + m.Write(pb) + if pb.Gauge != nil { + return pb.Gauge.GetValue() + } + if pb.Counter != nil { + return pb.Counter.GetValue() + } + if pb.Untyped != nil { + return pb.Untyped.GetValue() + } + panic(fmt.Errorf("collected a non-gauge/counter/untyped metric: %s", pb)) +} + +// CollectAndCompare registers the provided Collector with a newly created +// pedantic Registry. It then does the same as GatherAndCompare, gathering the +// metrics from the pedantic Registry. +func CollectAndCompare(c prometheus.Collector, expected io.Reader, metricNames ...string) error { + reg := prometheus.NewPedanticRegistry() + if err := reg.Register(c); err != nil { + return fmt.Errorf("registering collector failed: %s", err) + } + return GatherAndCompare(reg, expected, metricNames...) +} + +// GatherAndCompare gathers all metrics from the provided Gatherer and compares +// it to an expected output read from the provided Reader in the Prometheus text +// exposition format. If any metricNames are provided, only metrics with those +// names are compared. +func GatherAndCompare(g prometheus.Gatherer, expected io.Reader, metricNames ...string) error { + got, err := g.Gather() + if err != nil { + return fmt.Errorf("gathering metrics failed: %s", err) + } + if metricNames != nil { + got = filterMetrics(got, metricNames) + } + var tp expfmt.TextParser + wantRaw, err := tp.TextToMetricFamilies(expected) + if err != nil { + return fmt.Errorf("parsing expected metrics failed: %s", err) + } + want := internal.NormalizeMetricFamilies(wantRaw) + + return compare(got, want) +} + +// compare encodes both provided slices of metric families into the text format, +// compares their string message, and returns an error if they do not match. +// The error contains the encoded text of both the desired and the actual +// result. +func compare(got, want []*dto.MetricFamily) error { + var gotBuf, wantBuf bytes.Buffer + enc := expfmt.NewEncoder(&gotBuf, expfmt.FmtText) + for _, mf := range got { + if err := enc.Encode(mf); err != nil { + return fmt.Errorf("encoding gathered metrics failed: %s", err) + } + } + enc = expfmt.NewEncoder(&wantBuf, expfmt.FmtText) + for _, mf := range want { + if err := enc.Encode(mf); err != nil { + return fmt.Errorf("encoding expected metrics failed: %s", err) + } + } + + if wantBuf.String() != gotBuf.String() { + return fmt.Errorf(` +metric output does not match expectation; want: + +%s + +got: + +%s +`, wantBuf.String(), gotBuf.String()) + + } + return nil +} + +func filterMetrics(metrics []*dto.MetricFamily, names []string) []*dto.MetricFamily { + var filtered []*dto.MetricFamily + for _, m := range metrics { + for _, name := range names { + if m.GetName() == name { + filtered = append(filtered, m) + break + } + } + } + return filtered +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/testutil/testutil_test.go b/vendor/github.com/prometheus/client_golang/prometheus/testutil/testutil_test.go new file mode 100644 index 0000000000..f9600779e8 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/testutil/testutil_test.go @@ -0,0 +1,311 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutil + +import ( + "strings" + "testing" + + "github.com/prometheus/client_golang/prometheus" +) + +type untypedCollector struct{} + +func (u untypedCollector) Describe(c chan<- *prometheus.Desc) { + c <- prometheus.NewDesc("name", "help", nil, nil) +} + +func (u untypedCollector) Collect(c chan<- prometheus.Metric) { + c <- prometheus.MustNewConstMetric( + prometheus.NewDesc("name", "help", nil, nil), + prometheus.UntypedValue, + 2001, + ) +} + +func TestToFloat64(t *testing.T) { + gaugeWithAValueSet := prometheus.NewGauge(prometheus.GaugeOpts{}) + gaugeWithAValueSet.Set(3.14) + + counterVecWithOneElement := prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{"foo"}) + counterVecWithOneElement.WithLabelValues("bar").Inc() + + counterVecWithTwoElements := prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{"foo"}) + counterVecWithTwoElements.WithLabelValues("bar").Add(42) + counterVecWithTwoElements.WithLabelValues("baz").Inc() + + histogramVecWithOneElement := prometheus.NewHistogramVec(prometheus.HistogramOpts{}, []string{"foo"}) + histogramVecWithOneElement.WithLabelValues("bar").Observe(2.7) + + scenarios := map[string]struct { + collector prometheus.Collector + panics bool + want float64 + }{ + "simple counter": { + collector: prometheus.NewCounter(prometheus.CounterOpts{}), + panics: false, + want: 0, + }, + "simple gauge": { + collector: prometheus.NewGauge(prometheus.GaugeOpts{}), + panics: false, + want: 0, + }, + "simple untyped": { + collector: untypedCollector{}, + panics: false, + want: 2001, + }, + "simple histogram": { + collector: prometheus.NewHistogram(prometheus.HistogramOpts{}), + panics: true, + }, + "simple summary": { + collector: prometheus.NewSummary(prometheus.SummaryOpts{}), + panics: true, + }, + "simple gauge with an actual value set": { + collector: gaugeWithAValueSet, + panics: false, + want: 3.14, + }, + "counter vec with zero elements": { + collector: prometheus.NewCounterVec(prometheus.CounterOpts{}, nil), + panics: true, + }, + "counter vec with one element": { + collector: counterVecWithOneElement, + panics: false, + want: 1, + }, + "counter vec with two elements": { + collector: counterVecWithTwoElements, + panics: true, + }, + "histogram vec with one element": { + collector: histogramVecWithOneElement, + panics: true, + }, + } + + for n, s := range scenarios { + t.Run(n, func(t *testing.T) { + defer func() { + r := recover() + if r == nil && s.panics { + t.Error("expected panic") + } else if r != nil && !s.panics { + t.Error("unexpected panic: ", r) + } + // Any other combination is the expected outcome. + }() + if got := ToFloat64(s.collector); got != s.want { + t.Errorf("want %f, got %f", s.want, got) + } + }) + } +} + +func TestCollectAndCompare(t *testing.T) { + const metadata = ` + # HELP some_total A value that represents a counter. + # TYPE some_total counter + ` + + c := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "some_total", + Help: "A value that represents a counter.", + ConstLabels: prometheus.Labels{ + "label1": "value1", + }, + }) + c.Inc() + + expected := ` + + some_total{ label1 = "value1" } 1 + ` + + if err := CollectAndCompare(c, strings.NewReader(metadata+expected), "some_total"); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } +} + +func TestCollectAndCompareNoLabel(t *testing.T) { + const metadata = ` + # HELP some_total A value that represents a counter. + # TYPE some_total counter + ` + + c := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "some_total", + Help: "A value that represents a counter.", + }) + c.Inc() + + expected := ` + + some_total 1 + ` + + if err := CollectAndCompare(c, strings.NewReader(metadata+expected), "some_total"); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } +} + +func TestCollectAndCompareHistogram(t *testing.T) { + inputs := []struct { + name string + c prometheus.Collector + metadata string + expect string + labels []string + observation float64 + }{ + { + name: "Testing Histogram Collector", + c: prometheus.NewHistogram(prometheus.HistogramOpts{ + Name: "some_histogram", + Help: "An example of a histogram", + Buckets: []float64{1, 2, 3}, + }), + metadata: ` + # HELP some_histogram An example of a histogram + # TYPE some_histogram histogram + `, + expect: ` + some_histogram{le="1"} 0 + some_histogram{le="2"} 0 + some_histogram{le="3"} 1 + some_histogram_bucket{le="+Inf"} 1 + some_histogram_sum 2.5 + some_histogram_count 1 + + `, + observation: 2.5, + }, + { + name: "Testing HistogramVec Collector", + c: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: "some_histogram", + Help: "An example of a histogram", + Buckets: []float64{1, 2, 3}, + }, []string{"test"}), + + metadata: ` + # HELP some_histogram An example of a histogram + # TYPE some_histogram histogram + `, + expect: ` + some_histogram_bucket{test="test",le="1"} 0 + some_histogram_bucket{test="test",le="2"} 0 + some_histogram_bucket{test="test",le="3"} 1 + some_histogram_bucket{test="test",le="+Inf"} 1 + some_histogram_sum{test="test"} 2.5 + some_histogram_count{test="test"} 1 + + `, + observation: 2.5, + }, + } + + for _, input := range inputs { + switch collector := input.c.(type) { + case prometheus.Histogram: + collector.Observe(input.observation) + case *prometheus.HistogramVec: + collector.WithLabelValues("test").Observe(input.observation) + default: + t.Fatalf("unsuported collector tested") + + } + + t.Run(input.name, func(t *testing.T) { + if err := CollectAndCompare(input.c, strings.NewReader(input.metadata+input.expect)); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } + }) + + } +} + +func TestNoMetricFilter(t *testing.T) { + const metadata = ` + # HELP some_total A value that represents a counter. + # TYPE some_total counter + ` + + c := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "some_total", + Help: "A value that represents a counter.", + ConstLabels: prometheus.Labels{ + "label1": "value1", + }, + }) + c.Inc() + + expected := ` + some_total{label1="value1"} 1 + ` + + if err := CollectAndCompare(c, strings.NewReader(metadata+expected)); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } +} + +func TestMetricNotFound(t *testing.T) { + const metadata = ` + # HELP some_other_metric A value that represents a counter. + # TYPE some_other_metric counter + ` + + c := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "some_total", + Help: "A value that represents a counter.", + ConstLabels: prometheus.Labels{ + "label1": "value1", + }, + }) + c.Inc() + + expected := ` + some_other_metric{label1="value1"} 1 + ` + + expectedError := ` +metric output does not match expectation; want: + +# HELP some_other_metric A value that represents a counter. +# TYPE some_other_metric counter +some_other_metric{label1="value1"} 1 + + +got: + +# HELP some_total A value that represents a counter. +# TYPE some_total counter +some_total{label1="value1"} 1 + +` + + err := CollectAndCompare(c, strings.NewReader(metadata+expected)) + if err == nil { + t.Error("Expected error, got no error.") + } + + if err.Error() != expectedError { + t.Errorf("Expected\n%#+v\nGot:\n%#+v\n", expectedError, err.Error()) + } +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/timer.go b/vendor/github.com/prometheus/client_golang/prometheus/timer.go new file mode 100644 index 0000000000..8d5f105233 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/timer.go @@ -0,0 +1,54 @@ +// Copyright 2016 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import "time" + +// Timer is a helper type to time functions. Use NewTimer to create new +// instances. +type Timer struct { + begin time.Time + observer Observer +} + +// NewTimer creates a new Timer. The provided Observer is used to observe a +// duration in seconds. Timer is usually used to time a function call in the +// following way: +// func TimeMe() { +// timer := NewTimer(myHistogram) +// defer timer.ObserveDuration() +// // Do actual work. +// } +func NewTimer(o Observer) *Timer { + return &Timer{ + begin: time.Now(), + observer: o, + } +} + +// ObserveDuration records the duration passed since the Timer was created with +// NewTimer. It calls the Observe method of the Observer provided during +// construction with the duration in seconds as an argument. The observed +// duration is also returned. ObserveDuration is usually called with a defer +// statement. +// +// Note that this method is only guaranteed to never observe negative durations +// if used with Go1.9+. +func (t *Timer) ObserveDuration() time.Duration { + d := time.Since(t.begin) + if t.observer != nil { + t.observer.Observe(d.Seconds()) + } + return d +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/timer_test.go b/vendor/github.com/prometheus/client_golang/prometheus/timer_test.go new file mode 100644 index 0000000000..2949020686 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/timer_test.go @@ -0,0 +1,152 @@ +// Copyright 2016 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "testing" + + dto "github.com/prometheus/client_model/go" +) + +func TestTimerObserve(t *testing.T) { + var ( + his = NewHistogram(HistogramOpts{Name: "test_histogram"}) + sum = NewSummary(SummaryOpts{Name: "test_summary"}) + gauge = NewGauge(GaugeOpts{Name: "test_gauge"}) + ) + + func() { + hisTimer := NewTimer(his) + sumTimer := NewTimer(sum) + gaugeTimer := NewTimer(ObserverFunc(gauge.Set)) + defer hisTimer.ObserveDuration() + defer sumTimer.ObserveDuration() + defer gaugeTimer.ObserveDuration() + }() + + m := &dto.Metric{} + his.Write(m) + if want, got := uint64(1), m.GetHistogram().GetSampleCount(); want != got { + t.Errorf("want %d observations for histogram, got %d", want, got) + } + m.Reset() + sum.Write(m) + if want, got := uint64(1), m.GetSummary().GetSampleCount(); want != got { + t.Errorf("want %d observations for summary, got %d", want, got) + } + m.Reset() + gauge.Write(m) + if got := m.GetGauge().GetValue(); got <= 0 { + t.Errorf("want value > 0 for gauge, got %f", got) + } +} + +func TestTimerEmpty(t *testing.T) { + emptyTimer := NewTimer(nil) + emptyTimer.ObserveDuration() + // Do nothing, just demonstrate it works without panic. +} + +func TestTimerConditionalTiming(t *testing.T) { + var ( + his = NewHistogram(HistogramOpts{ + Name: "test_histogram", + }) + timeMe = true + m = &dto.Metric{} + ) + + timedFunc := func() { + timer := NewTimer(ObserverFunc(func(v float64) { + if timeMe { + his.Observe(v) + } + })) + defer timer.ObserveDuration() + } + + timedFunc() // This will time. + his.Write(m) + if want, got := uint64(1), m.GetHistogram().GetSampleCount(); want != got { + t.Errorf("want %d observations for histogram, got %d", want, got) + } + + timeMe = false + timedFunc() // This will not time again. + m.Reset() + his.Write(m) + if want, got := uint64(1), m.GetHistogram().GetSampleCount(); want != got { + t.Errorf("want %d observations for histogram, got %d", want, got) + } +} + +func TestTimerByOutcome(t *testing.T) { + var ( + his = NewHistogramVec( + HistogramOpts{Name: "test_histogram"}, + []string{"outcome"}, + ) + outcome = "foo" + m = &dto.Metric{} + ) + + timedFunc := func() { + timer := NewTimer(ObserverFunc(func(v float64) { + his.WithLabelValues(outcome).Observe(v) + })) + defer timer.ObserveDuration() + + if outcome == "foo" { + outcome = "bar" + return + } + outcome = "foo" + } + + timedFunc() + his.WithLabelValues("foo").(Histogram).Write(m) + if want, got := uint64(0), m.GetHistogram().GetSampleCount(); want != got { + t.Errorf("want %d observations for 'foo' histogram, got %d", want, got) + } + m.Reset() + his.WithLabelValues("bar").(Histogram).Write(m) + if want, got := uint64(1), m.GetHistogram().GetSampleCount(); want != got { + t.Errorf("want %d observations for 'bar' histogram, got %d", want, got) + } + + timedFunc() + m.Reset() + his.WithLabelValues("foo").(Histogram).Write(m) + if want, got := uint64(1), m.GetHistogram().GetSampleCount(); want != got { + t.Errorf("want %d observations for 'foo' histogram, got %d", want, got) + } + m.Reset() + his.WithLabelValues("bar").(Histogram).Write(m) + if want, got := uint64(1), m.GetHistogram().GetSampleCount(); want != got { + t.Errorf("want %d observations for 'bar' histogram, got %d", want, got) + } + + timedFunc() + m.Reset() + his.WithLabelValues("foo").(Histogram).Write(m) + if want, got := uint64(1), m.GetHistogram().GetSampleCount(); want != got { + t.Errorf("want %d observations for 'foo' histogram, got %d", want, got) + } + m.Reset() + his.WithLabelValues("bar").(Histogram).Write(m) + if want, got := uint64(2), m.GetHistogram().GetSampleCount(); want != got { + t.Errorf("want %d observations for 'bar' histogram, got %d", want, got) + } + +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/untyped.go b/vendor/github.com/prometheus/client_golang/prometheus/untyped.go index 5faf7e6e3e..0f9ce63f40 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/untyped.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/untyped.go @@ -13,108 +13,12 @@ package prometheus -// Untyped is a Metric that represents a single numerical value that can -// arbitrarily go up and down. -// -// An Untyped metric works the same as a Gauge. The only difference is that to -// no type information is implied. -// -// To create Untyped instances, use NewUntyped. -type Untyped interface { - Metric - Collector - - // Set sets the Untyped metric to an arbitrary value. - Set(float64) - // Inc increments the Untyped metric by 1. - Inc() - // Dec decrements the Untyped metric by 1. - Dec() - // Add adds the given value to the Untyped metric. (The value can be - // negative, resulting in a decrease.) - Add(float64) - // Sub subtracts the given value from the Untyped metric. (The value can - // be negative, resulting in an increase.) - Sub(float64) -} - // UntypedOpts is an alias for Opts. See there for doc comments. type UntypedOpts Opts -// NewUntyped creates a new Untyped metric from the provided UntypedOpts. -func NewUntyped(opts UntypedOpts) Untyped { - return newValue(NewDesc( - BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), - opts.Help, - nil, - opts.ConstLabels, - ), UntypedValue, 0) -} - -// UntypedVec is a Collector that bundles a set of Untyped metrics that all -// share the same Desc, but have different values for their variable -// labels. This is used if you want to count the same thing partitioned by -// various dimensions. Create instances with NewUntypedVec. -type UntypedVec struct { - *MetricVec -} - -// NewUntypedVec creates a new UntypedVec based on the provided UntypedOpts and -// partitioned by the given label names. At least one label name must be -// provided. -func NewUntypedVec(opts UntypedOpts, labelNames []string) *UntypedVec { - desc := NewDesc( - BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), - opts.Help, - labelNames, - opts.ConstLabels, - ) - return &UntypedVec{ - MetricVec: newMetricVec(desc, func(lvs ...string) Metric { - return newValue(desc, UntypedValue, 0, lvs...) - }), - } -} - -// GetMetricWithLabelValues replaces the method of the same name in -// MetricVec. The difference is that this method returns an Untyped and not a -// Metric so that no type conversion is required. -func (m *UntypedVec) GetMetricWithLabelValues(lvs ...string) (Untyped, error) { - metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...) - if metric != nil { - return metric.(Untyped), err - } - return nil, err -} - -// GetMetricWith replaces the method of the same name in MetricVec. The -// difference is that this method returns an Untyped and not a Metric so that no -// type conversion is required. -func (m *UntypedVec) GetMetricWith(labels Labels) (Untyped, error) { - metric, err := m.MetricVec.GetMetricWith(labels) - if metric != nil { - return metric.(Untyped), err - } - return nil, err -} - -// WithLabelValues works as GetMetricWithLabelValues, but panics where -// GetMetricWithLabelValues would have returned an error. By not returning an -// error, WithLabelValues allows shortcuts like -// myVec.WithLabelValues("404", "GET").Add(42) -func (m *UntypedVec) WithLabelValues(lvs ...string) Untyped { - return m.MetricVec.WithLabelValues(lvs...).(Untyped) -} - -// With works as GetMetricWith, but panics where GetMetricWithLabels would have -// returned an error. By not returning an error, With allows shortcuts like -// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42) -func (m *UntypedVec) With(labels Labels) Untyped { - return m.MetricVec.With(labels).(Untyped) -} - -// UntypedFunc is an Untyped whose value is determined at collect time by -// calling a provided function. +// UntypedFunc works like GaugeFunc but the collected metric is of type +// "Untyped". UntypedFunc is useful to mirror an external metric of unknown +// type. // // To create UntypedFunc instances, use NewUntypedFunc. type UntypedFunc interface { diff --git a/vendor/github.com/prometheus/client_golang/prometheus/value.go b/vendor/github.com/prometheus/client_golang/prometheus/value.go index a944c37754..eb248f1087 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/value.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/value.go @@ -14,15 +14,12 @@ package prometheus import ( - "errors" "fmt" - "math" "sort" - "sync/atomic" - - dto "github.com/prometheus/client_model/go" "github.com/golang/protobuf/proto" + + dto "github.com/prometheus/client_model/go" ) // ValueType is an enumeration of metric types that represent a simple value. @@ -36,77 +33,6 @@ const ( UntypedValue ) -var errInconsistentCardinality = errors.New("inconsistent label cardinality") - -// value is a generic metric for simple values. It implements Metric, Collector, -// Counter, Gauge, and Untyped. Its effective type is determined by -// ValueType. This is a low-level building block used by the library to back the -// implementations of Counter, Gauge, and Untyped. -type value struct { - // valBits containst the bits of the represented float64 value. It has - // to go first in the struct to guarantee alignment for atomic - // operations. http://golang.org/pkg/sync/atomic/#pkg-note-BUG - valBits uint64 - - selfCollector - - desc *Desc - valType ValueType - labelPairs []*dto.LabelPair -} - -// newValue returns a newly allocated value with the given Desc, ValueType, -// sample value and label values. It panics if the number of label -// values is different from the number of variable labels in Desc. -func newValue(desc *Desc, valueType ValueType, val float64, labelValues ...string) *value { - if len(labelValues) != len(desc.variableLabels) { - panic(errInconsistentCardinality) - } - result := &value{ - desc: desc, - valType: valueType, - valBits: math.Float64bits(val), - labelPairs: makeLabelPairs(desc, labelValues), - } - result.init(result) - return result -} - -func (v *value) Desc() *Desc { - return v.desc -} - -func (v *value) Set(val float64) { - atomic.StoreUint64(&v.valBits, math.Float64bits(val)) -} - -func (v *value) Inc() { - v.Add(1) -} - -func (v *value) Dec() { - v.Add(-1) -} - -func (v *value) Add(val float64) { - for { - oldBits := atomic.LoadUint64(&v.valBits) - newBits := math.Float64bits(math.Float64frombits(oldBits) + val) - if atomic.CompareAndSwapUint64(&v.valBits, oldBits, newBits) { - return - } - } -} - -func (v *value) Sub(val float64) { - v.Add(val * -1) -} - -func (v *value) Write(out *dto.Metric) error { - val := math.Float64frombits(atomic.LoadUint64(&v.valBits)) - return populateMetric(v.valType, val, v.labelPairs, out) -} - // valueFunc is a generic metric for simple values retrieved on collect time // from a function. It implements Metric and Collector. Its effective type is // determined by ValueType. This is a low-level building block used by the @@ -151,10 +77,14 @@ func (v *valueFunc) Write(out *dto.Metric) error { // operations. However, when implementing custom Collectors, it is useful as a // throw-away metric that is generated on the fly to send it to Prometheus in // the Collect method. NewConstMetric returns an error if the length of -// labelValues is not consistent with the variable labels in Desc. +// labelValues is not consistent with the variable labels in Desc or if Desc is +// invalid. func NewConstMetric(desc *Desc, valueType ValueType, value float64, labelValues ...string) (Metric, error) { - if len(desc.variableLabels) != len(labelValues) { - return nil, errInconsistentCardinality + if desc.err != nil { + return nil, desc.err + } + if err := validateLabelValues(labelValues, len(desc.variableLabels)); err != nil { + return nil, err } return &constMetric{ desc: desc, @@ -226,9 +156,7 @@ func makeLabelPairs(desc *Desc, labelValues []string) []*dto.LabelPair { Value: proto.String(labelValues[i]), }) } - for _, lp := range desc.constLabelPairs { - labelPairs = append(labelPairs, lp) - } - sort.Sort(LabelPairSorter(labelPairs)) + labelPairs = append(labelPairs, desc.constLabelPairs...) + sort.Sort(labelPairSorter(labelPairs)) return labelPairs } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/value_test.go b/vendor/github.com/prometheus/client_golang/prometheus/value_test.go new file mode 100644 index 0000000000..51867b5e47 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/value_test.go @@ -0,0 +1,56 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "fmt" + "testing" +) + +func TestNewConstMetricInvalidLabelValues(t *testing.T) { + testCases := []struct { + desc string + labels Labels + }{ + { + desc: "non utf8 label value", + labels: Labels{"a": "\xFF"}, + }, + { + desc: "not enough label values", + labels: Labels{}, + }, + { + desc: "too many label values", + labels: Labels{"a": "1", "b": "2"}, + }, + } + + for _, test := range testCases { + metricDesc := NewDesc( + "sample_value", + "sample value", + []string{"a"}, + Labels{}, + ) + + expectPanic(t, func() { + MustNewConstMetric(metricDesc, CounterValue, 0.3, "\xFF") + }, fmt.Sprintf("WithLabelValues: expected panic because: %s", test.desc)) + + if _, err := NewConstMetric(metricDesc, CounterValue, 0.3, "\xFF"); err == nil { + t.Errorf("NewConstMetric: expected error because: %s", test.desc) + } + } +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/vec.go b/vendor/github.com/prometheus/client_golang/prometheus/vec.go index 7f3eef9a46..14ed9e856d 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/vec.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/vec.go @@ -20,200 +20,253 @@ import ( "github.com/prometheus/common/model" ) -// MetricVec is a Collector to bundle metrics of the same name that -// differ in their label values. MetricVec is usually not used directly but as a -// building block for implementations of vectors of a given metric -// type. GaugeVec, CounterVec, SummaryVec, and UntypedVec are examples already -// provided in this package. -type MetricVec struct { - mtx sync.RWMutex // Protects the children. - children map[uint64][]metricWithLabelValues - desc *Desc - - newMetric func(labelValues ...string) Metric - hashAdd func(h uint64, s string) uint64 // replace hash function for testing collision handling +// metricVec is a Collector to bundle metrics of the same name that differ in +// their label values. metricVec is not used directly (and therefore +// unexported). It is used as a building block for implementations of vectors of +// a given metric type, like GaugeVec, CounterVec, SummaryVec, and HistogramVec. +// It also handles label currying. It uses basicMetricVec internally. +type metricVec struct { + *metricMap + + curry []curriedLabelValue + + // hashAdd and hashAddByte can be replaced for testing collision handling. + hashAdd func(h uint64, s string) uint64 hashAddByte func(h uint64, b byte) uint64 } -// newMetricVec returns an initialized MetricVec. The concrete value is -// returned for embedding into another struct. -func newMetricVec(desc *Desc, newMetric func(lvs ...string) Metric) *MetricVec { - return &MetricVec{ - children: map[uint64][]metricWithLabelValues{}, - desc: desc, - newMetric: newMetric, +// newMetricVec returns an initialized metricVec. +func newMetricVec(desc *Desc, newMetric func(lvs ...string) Metric) *metricVec { + return &metricVec{ + metricMap: &metricMap{ + metrics: map[uint64][]metricWithLabelValues{}, + desc: desc, + newMetric: newMetric, + }, hashAdd: hashAdd, hashAddByte: hashAddByte, } } -// metricWithLabelValues provides the metric and its label values for -// disambiguation on hash collision. -type metricWithLabelValues struct { - values []string - metric Metric -} +// DeleteLabelValues removes the metric where the variable labels are the same +// as those passed in as labels (same order as the VariableLabels in Desc). It +// returns true if a metric was deleted. +// +// It is not an error if the number of label values is not the same as the +// number of VariableLabels in Desc. However, such inconsistent label count can +// never match an actual metric, so the method will always return false in that +// case. +// +// Note that for more than one label value, this method is prone to mistakes +// caused by an incorrect order of arguments. Consider Delete(Labels) as an +// alternative to avoid that type of mistake. For higher label numbers, the +// latter has a much more readable (albeit more verbose) syntax, but it comes +// with a performance overhead (for creating and processing the Labels map). +// See also the CounterVec example. +func (m *metricVec) DeleteLabelValues(lvs ...string) bool { + h, err := m.hashLabelValues(lvs) + if err != nil { + return false + } -// Describe implements Collector. The length of the returned slice -// is always one. -func (m *MetricVec) Describe(ch chan<- *Desc) { - ch <- m.desc + return m.metricMap.deleteByHashWithLabelValues(h, lvs, m.curry) } -// Collect implements Collector. -func (m *MetricVec) Collect(ch chan<- Metric) { - m.mtx.RLock() - defer m.mtx.RUnlock() +// Delete deletes the metric where the variable labels are the same as those +// passed in as labels. It returns true if a metric was deleted. +// +// It is not an error if the number and names of the Labels are inconsistent +// with those of the VariableLabels in Desc. However, such inconsistent Labels +// can never match an actual metric, so the method will always return false in +// that case. +// +// This method is used for the same purpose as DeleteLabelValues(...string). See +// there for pros and cons of the two methods. +func (m *metricVec) Delete(labels Labels) bool { + h, err := m.hashLabels(labels) + if err != nil { + return false + } - for _, metrics := range m.children { - for _, metric := range metrics { - ch <- metric.metric + return m.metricMap.deleteByHashWithLabels(h, labels, m.curry) +} + +func (m *metricVec) curryWith(labels Labels) (*metricVec, error) { + var ( + newCurry []curriedLabelValue + oldCurry = m.curry + iCurry int + ) + for i, label := range m.desc.variableLabels { + val, ok := labels[label] + if iCurry < len(oldCurry) && oldCurry[iCurry].index == i { + if ok { + return nil, fmt.Errorf("label name %q is already curried", label) + } + newCurry = append(newCurry, oldCurry[iCurry]) + iCurry++ + } else { + if !ok { + continue // Label stays uncurried. + } + newCurry = append(newCurry, curriedLabelValue{i, val}) } } + if l := len(oldCurry) + len(labels) - len(newCurry); l > 0 { + return nil, fmt.Errorf("%d unknown label(s) found during currying", l) + } + + return &metricVec{ + metricMap: m.metricMap, + curry: newCurry, + hashAdd: m.hashAdd, + hashAddByte: m.hashAddByte, + }, nil } -// GetMetricWithLabelValues returns the Metric for the given slice of label -// values (same order as the VariableLabels in Desc). If that combination of -// label values is accessed for the first time, a new Metric is created. -// -// It is possible to call this method without using the returned Metric to only -// create the new Metric but leave it at its start value (e.g. a Summary or -// Histogram without any observations). See also the SummaryVec example. -// -// Keeping the Metric for later use is possible (and should be considered if -// performance is critical), but keep in mind that Reset, DeleteLabelValues and -// Delete can be used to delete the Metric from the MetricVec. In that case, the -// Metric will still exist, but it will not be exported anymore, even if a -// Metric with the same label values is created later. See also the CounterVec -// example. -// -// An error is returned if the number of label values is not the same as the -// number of VariableLabels in Desc. -// -// Note that for more than one label value, this method is prone to mistakes -// caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as -// an alternative to avoid that type of mistake. For higher label numbers, the -// latter has a much more readable (albeit more verbose) syntax, but it comes -// with a performance overhead (for creating and processing the Labels map). -// See also the GaugeVec example. -func (m *MetricVec) GetMetricWithLabelValues(lvs ...string) (Metric, error) { +func (m *metricVec) getMetricWithLabelValues(lvs ...string) (Metric, error) { h, err := m.hashLabelValues(lvs) if err != nil { return nil, err } - return m.getOrCreateMetricWithLabelValues(h, lvs), nil + return m.metricMap.getOrCreateMetricWithLabelValues(h, lvs, m.curry), nil } -// GetMetricWith returns the Metric for the given Labels map (the label names -// must match those of the VariableLabels in Desc). If that label map is -// accessed for the first time, a new Metric is created. Implications of -// creating a Metric without using it and keeping the Metric for later use are -// the same as for GetMetricWithLabelValues. -// -// An error is returned if the number and names of the Labels are inconsistent -// with those of the VariableLabels in Desc. -// -// This method is used for the same purpose as -// GetMetricWithLabelValues(...string). See there for pros and cons of the two -// methods. -func (m *MetricVec) GetMetricWith(labels Labels) (Metric, error) { +func (m *metricVec) getMetricWith(labels Labels) (Metric, error) { h, err := m.hashLabels(labels) if err != nil { return nil, err } - return m.getOrCreateMetricWithLabels(h, labels), nil + return m.metricMap.getOrCreateMetricWithLabels(h, labels, m.curry), nil } -// WithLabelValues works as GetMetricWithLabelValues, but panics if an error -// occurs. The method allows neat syntax like: -// httpReqs.WithLabelValues("404", "POST").Inc() -func (m *MetricVec) WithLabelValues(lvs ...string) Metric { - metric, err := m.GetMetricWithLabelValues(lvs...) - if err != nil { - panic(err) +func (m *metricVec) hashLabelValues(vals []string) (uint64, error) { + if err := validateLabelValues(vals, len(m.desc.variableLabels)-len(m.curry)); err != nil { + return 0, err + } + + var ( + h = hashNew() + curry = m.curry + iVals, iCurry int + ) + for i := 0; i < len(m.desc.variableLabels); i++ { + if iCurry < len(curry) && curry[iCurry].index == i { + h = m.hashAdd(h, curry[iCurry].value) + iCurry++ + } else { + h = m.hashAdd(h, vals[iVals]) + iVals++ + } + h = m.hashAddByte(h, model.SeparatorByte) } - return metric + return h, nil } -// With works as GetMetricWith, but panics if an error occurs. The method allows -// neat syntax like: -// httpReqs.With(Labels{"status":"404", "method":"POST"}).Inc() -func (m *MetricVec) With(labels Labels) Metric { - metric, err := m.GetMetricWith(labels) - if err != nil { - panic(err) +func (m *metricVec) hashLabels(labels Labels) (uint64, error) { + if err := validateValuesInLabels(labels, len(m.desc.variableLabels)-len(m.curry)); err != nil { + return 0, err } - return metric + + var ( + h = hashNew() + curry = m.curry + iCurry int + ) + for i, label := range m.desc.variableLabels { + val, ok := labels[label] + if iCurry < len(curry) && curry[iCurry].index == i { + if ok { + return 0, fmt.Errorf("label name %q is already curried", label) + } + h = m.hashAdd(h, curry[iCurry].value) + iCurry++ + } else { + if !ok { + return 0, fmt.Errorf("label name %q missing in label map", label) + } + h = m.hashAdd(h, val) + } + h = m.hashAddByte(h, model.SeparatorByte) + } + return h, nil } -// DeleteLabelValues removes the metric where the variable labels are the same -// as those passed in as labels (same order as the VariableLabels in Desc). It -// returns true if a metric was deleted. -// -// It is not an error if the number of label values is not the same as the -// number of VariableLabels in Desc. However, such inconsistent label count can -// never match an actual Metric, so the method will always return false in that -// case. -// -// Note that for more than one label value, this method is prone to mistakes -// caused by an incorrect order of arguments. Consider Delete(Labels) as an -// alternative to avoid that type of mistake. For higher label numbers, the -// latter has a much more readable (albeit more verbose) syntax, but it comes -// with a performance overhead (for creating and processing the Labels map). -// See also the CounterVec example. -func (m *MetricVec) DeleteLabelValues(lvs ...string) bool { - m.mtx.Lock() - defer m.mtx.Unlock() +// metricWithLabelValues provides the metric and its label values for +// disambiguation on hash collision. +type metricWithLabelValues struct { + values []string + metric Metric +} - h, err := m.hashLabelValues(lvs) - if err != nil { - return false +// curriedLabelValue sets the curried value for a label at the given index. +type curriedLabelValue struct { + index int + value string +} + +// metricMap is a helper for metricVec and shared between differently curried +// metricVecs. +type metricMap struct { + mtx sync.RWMutex // Protects metrics. + metrics map[uint64][]metricWithLabelValues + desc *Desc + newMetric func(labelValues ...string) Metric +} + +// Describe implements Collector. It will send exactly one Desc to the provided +// channel. +func (m *metricMap) Describe(ch chan<- *Desc) { + ch <- m.desc +} + +// Collect implements Collector. +func (m *metricMap) Collect(ch chan<- Metric) { + m.mtx.RLock() + defer m.mtx.RUnlock() + + for _, metrics := range m.metrics { + for _, metric := range metrics { + ch <- metric.metric + } } - return m.deleteByHashWithLabelValues(h, lvs) } -// Delete deletes the metric where the variable labels are the same as those -// passed in as labels. It returns true if a metric was deleted. -// -// It is not an error if the number and names of the Labels are inconsistent -// with those of the VariableLabels in the Desc of the MetricVec. However, such -// inconsistent Labels can never match an actual Metric, so the method will -// always return false in that case. -// -// This method is used for the same purpose as DeleteLabelValues(...string). See -// there for pros and cons of the two methods. -func (m *MetricVec) Delete(labels Labels) bool { +// Reset deletes all metrics in this vector. +func (m *metricMap) Reset() { m.mtx.Lock() defer m.mtx.Unlock() - h, err := m.hashLabels(labels) - if err != nil { - return false + for h := range m.metrics { + delete(m.metrics, h) } - - return m.deleteByHashWithLabels(h, labels) } // deleteByHashWithLabelValues removes the metric from the hash bucket h. If // there are multiple matches in the bucket, use lvs to select a metric and // remove only that metric. -func (m *MetricVec) deleteByHashWithLabelValues(h uint64, lvs []string) bool { - metrics, ok := m.children[h] +func (m *metricMap) deleteByHashWithLabelValues( + h uint64, lvs []string, curry []curriedLabelValue, +) bool { + m.mtx.Lock() + defer m.mtx.Unlock() + + metrics, ok := m.metrics[h] if !ok { return false } - i := m.findMetricWithLabelValues(metrics, lvs) + i := findMetricWithLabelValues(metrics, lvs, curry) if i >= len(metrics) { return false } if len(metrics) > 1 { - m.children[h] = append(metrics[:i], metrics[i+1:]...) + m.metrics[h] = append(metrics[:i], metrics[i+1:]...) } else { - delete(m.children, h) + delete(m.metrics, h) } return true } @@ -221,69 +274,38 @@ func (m *MetricVec) deleteByHashWithLabelValues(h uint64, lvs []string) bool { // deleteByHashWithLabels removes the metric from the hash bucket h. If there // are multiple matches in the bucket, use lvs to select a metric and remove // only that metric. -func (m *MetricVec) deleteByHashWithLabels(h uint64, labels Labels) bool { - metrics, ok := m.children[h] +func (m *metricMap) deleteByHashWithLabels( + h uint64, labels Labels, curry []curriedLabelValue, +) bool { + m.mtx.Lock() + defer m.mtx.Unlock() + + metrics, ok := m.metrics[h] if !ok { return false } - i := m.findMetricWithLabels(metrics, labels) + i := findMetricWithLabels(m.desc, metrics, labels, curry) if i >= len(metrics) { return false } if len(metrics) > 1 { - m.children[h] = append(metrics[:i], metrics[i+1:]...) + m.metrics[h] = append(metrics[:i], metrics[i+1:]...) } else { - delete(m.children, h) + delete(m.metrics, h) } return true } -// Reset deletes all metrics in this vector. -func (m *MetricVec) Reset() { - m.mtx.Lock() - defer m.mtx.Unlock() - - for h := range m.children { - delete(m.children, h) - } -} - -func (m *MetricVec) hashLabelValues(vals []string) (uint64, error) { - if len(vals) != len(m.desc.variableLabels) { - return 0, errInconsistentCardinality - } - h := hashNew() - for _, val := range vals { - h = m.hashAdd(h, val) - h = m.hashAddByte(h, model.SeparatorByte) - } - return h, nil -} - -func (m *MetricVec) hashLabels(labels Labels) (uint64, error) { - if len(labels) != len(m.desc.variableLabels) { - return 0, errInconsistentCardinality - } - h := hashNew() - for _, label := range m.desc.variableLabels { - val, ok := labels[label] - if !ok { - return 0, fmt.Errorf("label name %q missing in label map", label) - } - h = m.hashAdd(h, val) - h = m.hashAddByte(h, model.SeparatorByte) - } - return h, nil -} - // getOrCreateMetricWithLabelValues retrieves the metric by hash and label value // or creates it and returns the new one. // // This function holds the mutex. -func (m *MetricVec) getOrCreateMetricWithLabelValues(hash uint64, lvs []string) Metric { +func (m *metricMap) getOrCreateMetricWithLabelValues( + hash uint64, lvs []string, curry []curriedLabelValue, +) Metric { m.mtx.RLock() - metric, ok := m.getMetricWithLabelValues(hash, lvs) + metric, ok := m.getMetricWithHashAndLabelValues(hash, lvs, curry) m.mtx.RUnlock() if ok { return metric @@ -291,13 +313,11 @@ func (m *MetricVec) getOrCreateMetricWithLabelValues(hash uint64, lvs []string) m.mtx.Lock() defer m.mtx.Unlock() - metric, ok = m.getMetricWithLabelValues(hash, lvs) + metric, ok = m.getMetricWithHashAndLabelValues(hash, lvs, curry) if !ok { - // Copy to avoid allocation in case wo don't go down this code path. - copiedLVs := make([]string, len(lvs)) - copy(copiedLVs, lvs) - metric = m.newMetric(copiedLVs...) - m.children[hash] = append(m.children[hash], metricWithLabelValues{values: copiedLVs, metric: metric}) + inlinedLVs := inlineLabelValues(lvs, curry) + metric = m.newMetric(inlinedLVs...) + m.metrics[hash] = append(m.metrics[hash], metricWithLabelValues{values: inlinedLVs, metric: metric}) } return metric } @@ -306,9 +326,11 @@ func (m *MetricVec) getOrCreateMetricWithLabelValues(hash uint64, lvs []string) // or creates it and returns the new one. // // This function holds the mutex. -func (m *MetricVec) getOrCreateMetricWithLabels(hash uint64, labels Labels) Metric { +func (m *metricMap) getOrCreateMetricWithLabels( + hash uint64, labels Labels, curry []curriedLabelValue, +) Metric { m.mtx.RLock() - metric, ok := m.getMetricWithLabels(hash, labels) + metric, ok := m.getMetricWithHashAndLabels(hash, labels, curry) m.mtx.RUnlock() if ok { return metric @@ -316,33 +338,37 @@ func (m *MetricVec) getOrCreateMetricWithLabels(hash uint64, labels Labels) Metr m.mtx.Lock() defer m.mtx.Unlock() - metric, ok = m.getMetricWithLabels(hash, labels) + metric, ok = m.getMetricWithHashAndLabels(hash, labels, curry) if !ok { - lvs := m.extractLabelValues(labels) + lvs := extractLabelValues(m.desc, labels, curry) metric = m.newMetric(lvs...) - m.children[hash] = append(m.children[hash], metricWithLabelValues{values: lvs, metric: metric}) + m.metrics[hash] = append(m.metrics[hash], metricWithLabelValues{values: lvs, metric: metric}) } return metric } -// getMetricWithLabelValues gets a metric while handling possible collisions in -// the hash space. Must be called while holding read mutex. -func (m *MetricVec) getMetricWithLabelValues(h uint64, lvs []string) (Metric, bool) { - metrics, ok := m.children[h] +// getMetricWithHashAndLabelValues gets a metric while handling possible +// collisions in the hash space. Must be called while holding the read mutex. +func (m *metricMap) getMetricWithHashAndLabelValues( + h uint64, lvs []string, curry []curriedLabelValue, +) (Metric, bool) { + metrics, ok := m.metrics[h] if ok { - if i := m.findMetricWithLabelValues(metrics, lvs); i < len(metrics) { + if i := findMetricWithLabelValues(metrics, lvs, curry); i < len(metrics) { return metrics[i].metric, true } } return nil, false } -// getMetricWithLabels gets a metric while handling possible collisions in +// getMetricWithHashAndLabels gets a metric while handling possible collisions in // the hash space. Must be called while holding read mutex. -func (m *MetricVec) getMetricWithLabels(h uint64, labels Labels) (Metric, bool) { - metrics, ok := m.children[h] +func (m *metricMap) getMetricWithHashAndLabels( + h uint64, labels Labels, curry []curriedLabelValue, +) (Metric, bool) { + metrics, ok := m.metrics[h] if ok { - if i := m.findMetricWithLabels(metrics, labels); i < len(metrics) { + if i := findMetricWithLabels(m.desc, metrics, labels, curry); i < len(metrics) { return metrics[i].metric, true } } @@ -351,9 +377,11 @@ func (m *MetricVec) getMetricWithLabels(h uint64, labels Labels) (Metric, bool) // findMetricWithLabelValues returns the index of the matching metric or // len(metrics) if not found. -func (m *MetricVec) findMetricWithLabelValues(metrics []metricWithLabelValues, lvs []string) int { +func findMetricWithLabelValues( + metrics []metricWithLabelValues, lvs []string, curry []curriedLabelValue, +) int { for i, metric := range metrics { - if m.matchLabelValues(metric.values, lvs) { + if matchLabelValues(metric.values, lvs, curry) { return i } } @@ -362,32 +390,51 @@ func (m *MetricVec) findMetricWithLabelValues(metrics []metricWithLabelValues, l // findMetricWithLabels returns the index of the matching metric or len(metrics) // if not found. -func (m *MetricVec) findMetricWithLabels(metrics []metricWithLabelValues, labels Labels) int { +func findMetricWithLabels( + desc *Desc, metrics []metricWithLabelValues, labels Labels, curry []curriedLabelValue, +) int { for i, metric := range metrics { - if m.matchLabels(metric.values, labels) { + if matchLabels(desc, metric.values, labels, curry) { return i } } return len(metrics) } -func (m *MetricVec) matchLabelValues(values []string, lvs []string) bool { - if len(values) != len(lvs) { +func matchLabelValues(values []string, lvs []string, curry []curriedLabelValue) bool { + if len(values) != len(lvs)+len(curry) { return false } + var iLVs, iCurry int for i, v := range values { - if v != lvs[i] { + if iCurry < len(curry) && curry[iCurry].index == i { + if v != curry[iCurry].value { + return false + } + iCurry++ + continue + } + if v != lvs[iLVs] { return false } + iLVs++ } return true } -func (m *MetricVec) matchLabels(values []string, labels Labels) bool { - if len(labels) != len(values) { +func matchLabels(desc *Desc, values []string, labels Labels, curry []curriedLabelValue) bool { + if len(values) != len(labels)+len(curry) { return false } - for i, k := range m.desc.variableLabels { + iCurry := 0 + for i, k := range desc.variableLabels { + if iCurry < len(curry) && curry[iCurry].index == i { + if values[i] != curry[iCurry].value { + return false + } + iCurry++ + continue + } if values[i] != labels[k] { return false } @@ -395,10 +442,31 @@ func (m *MetricVec) matchLabels(values []string, labels Labels) bool { return true } -func (m *MetricVec) extractLabelValues(labels Labels) []string { - labelValues := make([]string, len(labels)) - for i, k := range m.desc.variableLabels { +func extractLabelValues(desc *Desc, labels Labels, curry []curriedLabelValue) []string { + labelValues := make([]string, len(labels)+len(curry)) + iCurry := 0 + for i, k := range desc.variableLabels { + if iCurry < len(curry) && curry[iCurry].index == i { + labelValues[i] = curry[iCurry].value + iCurry++ + continue + } labelValues[i] = labels[k] } return labelValues } + +func inlineLabelValues(lvs []string, curry []curriedLabelValue) []string { + labelValues := make([]string, len(lvs)+len(curry)) + var iCurry, iLVs int + for i := range labelValues { + if iCurry < len(curry) && curry[iCurry].index == i { + labelValues[i] = curry[iCurry].value + iCurry++ + continue + } + labelValues[i] = lvs[iLVs] + iLVs++ + } + return labelValues +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/vec_test.go b/vendor/github.com/prometheus/client_golang/prometheus/vec_test.go index 445a6b39fe..bd18a9f4e3 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/vec_test.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/vec_test.go @@ -21,8 +21,8 @@ import ( ) func TestDelete(t *testing.T) { - vec := NewUntypedVec( - UntypedOpts{ + vec := NewGaugeVec( + GaugeOpts{ Name: "test", Help: "helpless", }, @@ -32,8 +32,8 @@ func TestDelete(t *testing.T) { } func TestDeleteWithCollisions(t *testing.T) { - vec := NewUntypedVec( - UntypedOpts{ + vec := NewGaugeVec( + GaugeOpts{ Name: "test", Help: "helpless", }, @@ -44,12 +44,12 @@ func TestDeleteWithCollisions(t *testing.T) { testDelete(t, vec) } -func testDelete(t *testing.T, vec *UntypedVec) { +func testDelete(t *testing.T, vec *GaugeVec) { if got, want := vec.Delete(Labels{"l1": "v1", "l2": "v2"}), false; got != want { t.Errorf("got %v, want %v", got, want) } - vec.With(Labels{"l1": "v1", "l2": "v2"}).(Untyped).Set(42) + vec.With(Labels{"l1": "v1", "l2": "v2"}).(Gauge).Set(42) if got, want := vec.Delete(Labels{"l1": "v1", "l2": "v2"}), true; got != want { t.Errorf("got %v, want %v", got, want) } @@ -57,7 +57,7 @@ func testDelete(t *testing.T, vec *UntypedVec) { t.Errorf("got %v, want %v", got, want) } - vec.With(Labels{"l1": "v1", "l2": "v2"}).(Untyped).Set(42) + vec.With(Labels{"l1": "v1", "l2": "v2"}).(Gauge).Set(42) if got, want := vec.Delete(Labels{"l2": "v2", "l1": "v1"}), true; got != want { t.Errorf("got %v, want %v", got, want) } @@ -65,7 +65,7 @@ func testDelete(t *testing.T, vec *UntypedVec) { t.Errorf("got %v, want %v", got, want) } - vec.With(Labels{"l1": "v1", "l2": "v2"}).(Untyped).Set(42) + vec.With(Labels{"l1": "v1", "l2": "v2"}).(Gauge).Set(42) if got, want := vec.Delete(Labels{"l2": "v1", "l1": "v2"}), false; got != want { t.Errorf("got %v, want %v", got, want) } @@ -75,8 +75,8 @@ func testDelete(t *testing.T, vec *UntypedVec) { } func TestDeleteLabelValues(t *testing.T) { - vec := NewUntypedVec( - UntypedOpts{ + vec := NewGaugeVec( + GaugeOpts{ Name: "test", Help: "helpless", }, @@ -86,8 +86,8 @@ func TestDeleteLabelValues(t *testing.T) { } func TestDeleteLabelValuesWithCollisions(t *testing.T) { - vec := NewUntypedVec( - UntypedOpts{ + vec := NewGaugeVec( + GaugeOpts{ Name: "test", Help: "helpless", }, @@ -98,13 +98,13 @@ func TestDeleteLabelValuesWithCollisions(t *testing.T) { testDeleteLabelValues(t, vec) } -func testDeleteLabelValues(t *testing.T, vec *UntypedVec) { +func testDeleteLabelValues(t *testing.T, vec *GaugeVec) { if got, want := vec.DeleteLabelValues("v1", "v2"), false; got != want { t.Errorf("got %v, want %v", got, want) } - vec.With(Labels{"l1": "v1", "l2": "v2"}).(Untyped).Set(42) - vec.With(Labels{"l1": "v1", "l2": "v3"}).(Untyped).Set(42) // Add junk data for collision. + vec.With(Labels{"l1": "v1", "l2": "v2"}).(Gauge).Set(42) + vec.With(Labels{"l1": "v1", "l2": "v3"}).(Gauge).Set(42) // Add junk data for collision. if got, want := vec.DeleteLabelValues("v1", "v2"), true; got != want { t.Errorf("got %v, want %v", got, want) } @@ -115,7 +115,7 @@ func testDeleteLabelValues(t *testing.T, vec *UntypedVec) { t.Errorf("got %v, want %v", got, want) } - vec.With(Labels{"l1": "v1", "l2": "v2"}).(Untyped).Set(42) + vec.With(Labels{"l1": "v1", "l2": "v2"}).(Gauge).Set(42) // Delete out of order. if got, want := vec.DeleteLabelValues("v2", "v1"), false; got != want { t.Errorf("got %v, want %v", got, want) @@ -126,8 +126,8 @@ func testDeleteLabelValues(t *testing.T, vec *UntypedVec) { } func TestMetricVec(t *testing.T) { - vec := NewUntypedVec( - UntypedOpts{ + vec := NewGaugeVec( + GaugeOpts{ Name: "test", Help: "helpless", }, @@ -137,8 +137,8 @@ func TestMetricVec(t *testing.T) { } func TestMetricVecWithCollisions(t *testing.T) { - vec := NewUntypedVec( - UntypedOpts{ + vec := NewGaugeVec( + GaugeOpts{ Name: "test", Help: "helpless", }, @@ -149,7 +149,7 @@ func TestMetricVecWithCollisions(t *testing.T) { testMetricVec(t, vec) } -func testMetricVec(t *testing.T, vec *UntypedVec) { +func testMetricVec(t *testing.T, vec *GaugeVec) { vec.Reset() // Actually test Reset now! var pair [2]string @@ -162,11 +162,11 @@ func testMetricVec(t *testing.T, vec *UntypedVec) { vec.WithLabelValues(pair[0], pair[1]).Inc() expected[[2]string{"v1", "v2"}]++ - vec.WithLabelValues("v1", "v2").(Untyped).Inc() + vec.WithLabelValues("v1", "v2").(Gauge).Inc() } var total int - for _, metrics := range vec.children { + for _, metrics := range vec.metricMap.metrics { for _, metric := range metrics { total++ copy(pair[:], metric.values) @@ -175,7 +175,7 @@ func testMetricVec(t *testing.T, vec *UntypedVec) { if err := metric.metric.Write(&metricOut); err != nil { t.Fatal(err) } - actual := *metricOut.Untyped.Value + actual := *metricOut.Gauge.Value var actualPair [2]string for i, label := range metricOut.Label { @@ -201,7 +201,7 @@ func testMetricVec(t *testing.T, vec *UntypedVec) { vec.Reset() - if len(vec.children) > 0 { + if len(vec.metricMap.metrics) > 0 { t.Fatalf("reset failed") } } @@ -239,10 +239,233 @@ func TestCounterVecEndToEndWithCollision(t *testing.T) { } } +func TestCurryVec(t *testing.T) { + vec := NewCounterVec( + CounterOpts{ + Name: "test", + Help: "helpless", + }, + []string{"one", "two", "three"}, + ) + testCurryVec(t, vec) +} + +func TestCurryVecWithCollisions(t *testing.T) { + vec := NewCounterVec( + CounterOpts{ + Name: "test", + Help: "helpless", + }, + []string{"one", "two", "three"}, + ) + vec.hashAdd = func(h uint64, s string) uint64 { return 1 } + vec.hashAddByte = func(h uint64, b byte) uint64 { return 1 } + testCurryVec(t, vec) +} + +func testCurryVec(t *testing.T, vec *CounterVec) { + + assertMetrics := func(t *testing.T) { + n := 0 + for _, m := range vec.metricMap.metrics { + n += len(m) + } + if n != 2 { + t.Error("expected two metrics, got", n) + } + m := &dto.Metric{} + c1, err := vec.GetMetricWithLabelValues("1", "2", "3") + if err != nil { + t.Fatal("unexpected error getting metric:", err) + } + c1.Write(m) + if want, got := 1., m.GetCounter().GetValue(); want != got { + t.Errorf("want %f as counter value, got %f", want, got) + } + m.Reset() + c2, err := vec.GetMetricWithLabelValues("11", "22", "33") + if err != nil { + t.Fatal("unexpected error getting metric:", err) + } + c2.Write(m) + if want, got := 1., m.GetCounter().GetValue(); want != got { + t.Errorf("want %f as counter value, got %f", want, got) + } + } + + assertNoMetric := func(t *testing.T) { + if n := len(vec.metricMap.metrics); n != 0 { + t.Error("expected no metrics, got", n) + } + } + + t.Run("zero labels", func(t *testing.T) { + c1 := vec.MustCurryWith(nil) + c2 := vec.MustCurryWith(nil) + c1.WithLabelValues("1", "2", "3").Inc() + c2.With(Labels{"one": "11", "two": "22", "three": "33"}).Inc() + assertMetrics(t) + if !c1.Delete(Labels{"one": "1", "two": "2", "three": "3"}) { + t.Error("deletion failed") + } + if !c2.DeleteLabelValues("11", "22", "33") { + t.Error("deletion failed") + } + assertNoMetric(t) + }) + t.Run("first label", func(t *testing.T) { + c1 := vec.MustCurryWith(Labels{"one": "1"}) + c2 := vec.MustCurryWith(Labels{"one": "11"}) + c1.WithLabelValues("2", "3").Inc() + c2.With(Labels{"two": "22", "three": "33"}).Inc() + assertMetrics(t) + if c1.Delete(Labels{"two": "22", "three": "33"}) { + t.Error("deletion unexpectedly succeeded") + } + if c2.DeleteLabelValues("2", "3") { + t.Error("deletion unexpectedly succeeded") + } + if !c1.Delete(Labels{"two": "2", "three": "3"}) { + t.Error("deletion failed") + } + if !c2.DeleteLabelValues("22", "33") { + t.Error("deletion failed") + } + assertNoMetric(t) + }) + t.Run("middle label", func(t *testing.T) { + c1 := vec.MustCurryWith(Labels{"two": "2"}) + c2 := vec.MustCurryWith(Labels{"two": "22"}) + c1.WithLabelValues("1", "3").Inc() + c2.With(Labels{"one": "11", "three": "33"}).Inc() + assertMetrics(t) + if c1.Delete(Labels{"one": "11", "three": "33"}) { + t.Error("deletion unexpectedly succeeded") + } + if c2.DeleteLabelValues("1", "3") { + t.Error("deletion unexpectedly succeeded") + } + if !c1.Delete(Labels{"one": "1", "three": "3"}) { + t.Error("deletion failed") + } + if !c2.DeleteLabelValues("11", "33") { + t.Error("deletion failed") + } + assertNoMetric(t) + }) + t.Run("last label", func(t *testing.T) { + c1 := vec.MustCurryWith(Labels{"three": "3"}) + c2 := vec.MustCurryWith(Labels{"three": "33"}) + c1.WithLabelValues("1", "2").Inc() + c2.With(Labels{"one": "11", "two": "22"}).Inc() + assertMetrics(t) + if c1.Delete(Labels{"two": "22", "one": "11"}) { + t.Error("deletion unexpectedly succeeded") + } + if c2.DeleteLabelValues("1", "2") { + t.Error("deletion unexpectedly succeeded") + } + if !c1.Delete(Labels{"two": "2", "one": "1"}) { + t.Error("deletion failed") + } + if !c2.DeleteLabelValues("11", "22") { + t.Error("deletion failed") + } + assertNoMetric(t) + }) + t.Run("two labels", func(t *testing.T) { + c1 := vec.MustCurryWith(Labels{"three": "3", "one": "1"}) + c2 := vec.MustCurryWith(Labels{"three": "33", "one": "11"}) + c1.WithLabelValues("2").Inc() + c2.With(Labels{"two": "22"}).Inc() + assertMetrics(t) + if c1.Delete(Labels{"two": "22"}) { + t.Error("deletion unexpectedly succeeded") + } + if c2.DeleteLabelValues("2") { + t.Error("deletion unexpectedly succeeded") + } + if !c1.Delete(Labels{"two": "2"}) { + t.Error("deletion failed") + } + if !c2.DeleteLabelValues("22") { + t.Error("deletion failed") + } + assertNoMetric(t) + }) + t.Run("all labels", func(t *testing.T) { + c1 := vec.MustCurryWith(Labels{"three": "3", "two": "2", "one": "1"}) + c2 := vec.MustCurryWith(Labels{"three": "33", "one": "11", "two": "22"}) + c1.WithLabelValues().Inc() + c2.With(nil).Inc() + assertMetrics(t) + if !c1.Delete(Labels{}) { + t.Error("deletion failed") + } + if !c2.DeleteLabelValues() { + t.Error("deletion failed") + } + assertNoMetric(t) + }) + t.Run("double curry", func(t *testing.T) { + c1 := vec.MustCurryWith(Labels{"three": "3"}).MustCurryWith(Labels{"one": "1"}) + c2 := vec.MustCurryWith(Labels{"three": "33"}).MustCurryWith(Labels{"one": "11"}) + c1.WithLabelValues("2").Inc() + c2.With(Labels{"two": "22"}).Inc() + assertMetrics(t) + if c1.Delete(Labels{"two": "22"}) { + t.Error("deletion unexpectedly succeeded") + } + if c2.DeleteLabelValues("2") { + t.Error("deletion unexpectedly succeeded") + } + if !c1.Delete(Labels{"two": "2"}) { + t.Error("deletion failed") + } + if !c2.DeleteLabelValues("22") { + t.Error("deletion failed") + } + assertNoMetric(t) + }) + t.Run("use already curried label", func(t *testing.T) { + c1 := vec.MustCurryWith(Labels{"three": "3"}) + if _, err := c1.GetMetricWithLabelValues("1", "2", "3"); err == nil { + t.Error("expected error when using already curried label") + } + if _, err := c1.GetMetricWith(Labels{"one": "1", "two": "2", "three": "3"}); err == nil { + t.Error("expected error when using already curried label") + } + assertNoMetric(t) + c1.WithLabelValues("1", "2").Inc() + if c1.Delete(Labels{"one": "1", "two": "2", "three": "3"}) { + t.Error("deletion unexpectedly succeeded") + } + if !c1.Delete(Labels{"one": "1", "two": "2"}) { + t.Error("deletion failed") + } + assertNoMetric(t) + }) + t.Run("curry already curried label", func(t *testing.T) { + if _, err := vec.MustCurryWith(Labels{"three": "3"}).CurryWith(Labels{"three": "33"}); err == nil { + t.Error("currying unexpectedly succeeded") + } else if err.Error() != `label name "three" is already curried` { + t.Error("currying returned unexpected error:", err) + } + + }) + t.Run("unknown label", func(t *testing.T) { + if _, err := vec.CurryWith(Labels{"foo": "bar"}); err == nil { + t.Error("currying unexpectedly succeeded") + } else if err.Error() != "1 unknown label(s) found during currying" { + t.Error("currying returned unexpected error:", err) + } + }) +} + func BenchmarkMetricVecWithLabelValuesBasic(b *testing.B) { benchmarkMetricVecWithLabelValues(b, map[string][]string{ - "l1": []string{"onevalue"}, - "l2": []string{"twovalue"}, + "l1": {"onevalue"}, + "l2": {"twovalue"}, }) } @@ -290,8 +513,8 @@ func benchmarkMetricVecWithLabelValues(b *testing.B, labels map[string][]string) } values := make([]string, len(labels)) // Value cache for permutations. - vec := NewUntypedVec( - UntypedOpts{ + vec := NewGaugeVec( + GaugeOpts{ Name: "test", Help: "helpless", }, diff --git a/vendor/github.com/prometheus/client_golang/prometheus/wrap.go b/vendor/github.com/prometheus/client_golang/prometheus/wrap.go new file mode 100644 index 0000000000..49159bf3eb --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/wrap.go @@ -0,0 +1,179 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "fmt" + "sort" + + "github.com/golang/protobuf/proto" + + dto "github.com/prometheus/client_model/go" +) + +// WrapRegistererWith returns a Registerer wrapping the provided +// Registerer. Collectors registered with the returned Registerer will be +// registered with the wrapped Registerer in a modified way. The modified +// Collector adds the provided Labels to all Metrics it collects (as +// ConstLabels). The Metrics collected by the unmodified Collector must not +// duplicate any of those labels. +// +// WrapRegistererWith provides a way to add fixed labels to a subset of +// Collectors. It should not be used to add fixed labels to all metrics exposed. +// +// The Collector example demonstrates a use of WrapRegistererWith. +func WrapRegistererWith(labels Labels, reg Registerer) Registerer { + return &wrappingRegisterer{ + wrappedRegisterer: reg, + labels: labels, + } +} + +// WrapRegistererWithPrefix returns a Registerer wrapping the provided +// Registerer. Collectors registered with the returned Registerer will be +// registered with the wrapped Registerer in a modified way. The modified +// Collector adds the provided prefix to the name of all Metrics it collects. +// +// WrapRegistererWithPrefix is useful to have one place to prefix all metrics of +// a sub-system. To make this work, register metrics of the sub-system with the +// wrapping Registerer returned by WrapRegistererWithPrefix. It is rarely useful +// to use the same prefix for all metrics exposed. In particular, do not prefix +// metric names that are standardized across applications, as that would break +// horizontal monitoring, for example the metrics provided by the Go collector +// (see NewGoCollector) and the process collector (see NewProcessCollector). (In +// fact, those metrics are already prefixed with “go_” or “process_”, +// respectively.) +func WrapRegistererWithPrefix(prefix string, reg Registerer) Registerer { + return &wrappingRegisterer{ + wrappedRegisterer: reg, + prefix: prefix, + } +} + +type wrappingRegisterer struct { + wrappedRegisterer Registerer + prefix string + labels Labels +} + +func (r *wrappingRegisterer) Register(c Collector) error { + return r.wrappedRegisterer.Register(&wrappingCollector{ + wrappedCollector: c, + prefix: r.prefix, + labels: r.labels, + }) +} + +func (r *wrappingRegisterer) MustRegister(cs ...Collector) { + for _, c := range cs { + if err := r.Register(c); err != nil { + panic(err) + } + } +} + +func (r *wrappingRegisterer) Unregister(c Collector) bool { + return r.wrappedRegisterer.Unregister(&wrappingCollector{ + wrappedCollector: c, + prefix: r.prefix, + labels: r.labels, + }) +} + +type wrappingCollector struct { + wrappedCollector Collector + prefix string + labels Labels +} + +func (c *wrappingCollector) Collect(ch chan<- Metric) { + wrappedCh := make(chan Metric) + go func() { + c.wrappedCollector.Collect(wrappedCh) + close(wrappedCh) + }() + for m := range wrappedCh { + ch <- &wrappingMetric{ + wrappedMetric: m, + prefix: c.prefix, + labels: c.labels, + } + } +} + +func (c *wrappingCollector) Describe(ch chan<- *Desc) { + wrappedCh := make(chan *Desc) + go func() { + c.wrappedCollector.Describe(wrappedCh) + close(wrappedCh) + }() + for desc := range wrappedCh { + ch <- wrapDesc(desc, c.prefix, c.labels) + } +} + +type wrappingMetric struct { + wrappedMetric Metric + prefix string + labels Labels +} + +func (m *wrappingMetric) Desc() *Desc { + return wrapDesc(m.wrappedMetric.Desc(), m.prefix, m.labels) +} + +func (m *wrappingMetric) Write(out *dto.Metric) error { + if err := m.wrappedMetric.Write(out); err != nil { + return err + } + if len(m.labels) == 0 { + // No wrapping labels. + return nil + } + for ln, lv := range m.labels { + out.Label = append(out.Label, &dto.LabelPair{ + Name: proto.String(ln), + Value: proto.String(lv), + }) + } + sort.Sort(labelPairSorter(out.Label)) + return nil +} + +func wrapDesc(desc *Desc, prefix string, labels Labels) *Desc { + constLabels := Labels{} + for _, lp := range desc.constLabelPairs { + constLabels[*lp.Name] = *lp.Value + } + for ln, lv := range labels { + if _, alreadyUsed := constLabels[ln]; alreadyUsed { + return &Desc{ + fqName: desc.fqName, + help: desc.help, + variableLabels: desc.variableLabels, + constLabelPairs: desc.constLabelPairs, + err: fmt.Errorf("attempted wrapping with already existing label name %q", ln), + } + } + constLabels[ln] = lv + } + // NewDesc will do remaining validations. + newDesc := NewDesc(prefix+desc.fqName, desc.help, desc.variableLabels, constLabels) + // Propagate errors if there was any. This will override any errer + // created by NewDesc above, i.e. earlier errors get precedence. + if desc.err != nil { + newDesc.err = desc.err + } + return newDesc +} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/wrap_test.go b/vendor/github.com/prometheus/client_golang/prometheus/wrap_test.go new file mode 100644 index 0000000000..28e24704ed --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/prometheus/wrap_test.go @@ -0,0 +1,322 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/golang/protobuf/proto" + + dto "github.com/prometheus/client_model/go" +) + +// uncheckedCollector wraps a Collector but its Describe method yields no Desc. +type uncheckedCollector struct { + c Collector +} + +func (u uncheckedCollector) Describe(_ chan<- *Desc) {} +func (u uncheckedCollector) Collect(c chan<- Metric) { + u.c.Collect(c) +} + +func toMetricFamilies(cs ...Collector) []*dto.MetricFamily { + reg := NewRegistry() + reg.MustRegister(cs...) + out, err := reg.Gather() + if err != nil { + panic(err) + } + return out +} + +func TestWrap(t *testing.T) { + + simpleCnt := NewCounter(CounterOpts{ + Name: "simpleCnt", + Help: "helpSimpleCnt", + }) + simpleCnt.Inc() + + simpleGge := NewGauge(GaugeOpts{ + Name: "simpleGge", + Help: "helpSimpleGge", + }) + simpleGge.Set(3.14) + + preCnt := NewCounter(CounterOpts{ + Name: "pre_simpleCnt", + Help: "helpSimpleCnt", + }) + preCnt.Inc() + + barLabeledCnt := NewCounter(CounterOpts{ + Name: "simpleCnt", + Help: "helpSimpleCnt", + ConstLabels: Labels{"foo": "bar"}, + }) + barLabeledCnt.Inc() + + bazLabeledCnt := NewCounter(CounterOpts{ + Name: "simpleCnt", + Help: "helpSimpleCnt", + ConstLabels: Labels{"foo": "baz"}, + }) + bazLabeledCnt.Inc() + + labeledPreCnt := NewCounter(CounterOpts{ + Name: "pre_simpleCnt", + Help: "helpSimpleCnt", + ConstLabels: Labels{"foo": "bar"}, + }) + labeledPreCnt.Inc() + + twiceLabeledPreCnt := NewCounter(CounterOpts{ + Name: "pre_simpleCnt", + Help: "helpSimpleCnt", + ConstLabels: Labels{"foo": "bar", "dings": "bums"}, + }) + twiceLabeledPreCnt.Inc() + + barLabeledUncheckedCollector := uncheckedCollector{barLabeledCnt} + + scenarios := map[string]struct { + prefix string // First wrap with this prefix. + labels Labels // Then wrap the result with these labels. + labels2 Labels // If any, wrap the prefix-wrapped one again. + preRegister []Collector + toRegister []struct { // If there are any labels2, register every other with that one. + collector Collector + registrationFails bool + } + gatherFails bool + output []Collector + }{ + "wrap nothing": { + prefix: "pre_", + labels: Labels{"foo": "bar"}, + }, + "wrap with nothing": { + preRegister: []Collector{simpleGge}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{simpleCnt, false}}, + output: []Collector{simpleGge, simpleCnt}, + }, + "wrap counter with prefix": { + prefix: "pre_", + preRegister: []Collector{simpleGge}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{simpleCnt, false}}, + output: []Collector{simpleGge, preCnt}, + }, + "wrap counter with label pair": { + labels: Labels{"foo": "bar"}, + preRegister: []Collector{simpleGge}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{simpleCnt, false}}, + output: []Collector{simpleGge, barLabeledCnt}, + }, + "wrap counter with label pair and prefix": { + prefix: "pre_", + labels: Labels{"foo": "bar"}, + preRegister: []Collector{simpleGge}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{simpleCnt, false}}, + output: []Collector{simpleGge, labeledPreCnt}, + }, + "wrap counter with invalid prefix": { + prefix: "1+1", + preRegister: []Collector{simpleGge}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{simpleCnt, true}}, + output: []Collector{simpleGge}, + }, + "wrap counter with invalid label": { + preRegister: []Collector{simpleGge}, + labels: Labels{"42": "bar"}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{simpleCnt, true}}, + output: []Collector{simpleGge}, + }, + "counter registered twice but wrapped with different label values": { + labels: Labels{"foo": "bar"}, + labels2: Labels{"foo": "baz"}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{simpleCnt, false}, {simpleCnt, false}}, + output: []Collector{barLabeledCnt, bazLabeledCnt}, + }, + "counter registered twice but wrapped with different inconsistent label values": { + labels: Labels{"foo": "bar"}, + labels2: Labels{"bar": "baz"}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{simpleCnt, false}, {simpleCnt, true}}, + output: []Collector{barLabeledCnt}, + }, + "wrap counter with prefix and two labels": { + prefix: "pre_", + labels: Labels{"foo": "bar", "dings": "bums"}, + preRegister: []Collector{simpleGge}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{simpleCnt, false}}, + output: []Collector{simpleGge, twiceLabeledPreCnt}, + }, + "wrap labeled counter with prefix and another label": { + prefix: "pre_", + labels: Labels{"dings": "bums"}, + preRegister: []Collector{simpleGge}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{barLabeledCnt, false}}, + output: []Collector{simpleGge, twiceLabeledPreCnt}, + }, + "wrap labeled counter with prefix and inconsistent label": { + prefix: "pre_", + labels: Labels{"foo": "bums"}, + preRegister: []Collector{simpleGge}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{barLabeledCnt, true}}, + output: []Collector{simpleGge}, + }, + "wrap labeled counter with prefix and the same label again": { + prefix: "pre_", + labels: Labels{"foo": "bar"}, + preRegister: []Collector{simpleGge}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{barLabeledCnt, true}}, + output: []Collector{simpleGge}, + }, + "wrap labeled unchecked collector with prefix and another label": { + prefix: "pre_", + labels: Labels{"dings": "bums"}, + preRegister: []Collector{simpleGge}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{barLabeledUncheckedCollector, false}}, + output: []Collector{simpleGge, twiceLabeledPreCnt}, + }, + "wrap labeled unchecked collector with prefix and inconsistent label": { + prefix: "pre_", + labels: Labels{"foo": "bums"}, + preRegister: []Collector{simpleGge}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{barLabeledUncheckedCollector, false}}, + gatherFails: true, + output: []Collector{simpleGge}, + }, + "wrap labeled unchecked collector with prefix and the same label again": { + prefix: "pre_", + labels: Labels{"foo": "bar"}, + preRegister: []Collector{simpleGge}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{barLabeledUncheckedCollector, false}}, + gatherFails: true, + output: []Collector{simpleGge}, + }, + "wrap labeled unchecked collector with prefix and another label resulting in collision with pre-registered counter": { + prefix: "pre_", + labels: Labels{"dings": "bums"}, + preRegister: []Collector{twiceLabeledPreCnt}, + toRegister: []struct { + collector Collector + registrationFails bool + }{{barLabeledUncheckedCollector, false}}, + gatherFails: true, + output: []Collector{twiceLabeledPreCnt}, + }, + } + + for n, s := range scenarios { + t.Run(n, func(t *testing.T) { + reg := NewPedanticRegistry() + for _, c := range s.preRegister { + if err := reg.Register(c); err != nil { + t.Fatal("error registering with unwrapped registry:", err) + } + } + preReg := WrapRegistererWithPrefix(s.prefix, reg) + lReg := WrapRegistererWith(s.labels, preReg) + l2Reg := WrapRegistererWith(s.labels2, preReg) + for i, tr := range s.toRegister { + var err error + if i%2 != 0 && len(s.labels2) != 0 { + err = l2Reg.Register(tr.collector) + } else { + err = lReg.Register(tr.collector) + } + if tr.registrationFails && err == nil { + t.Fatalf("registration with wrapping registry unexpectedly succeded for collector #%d", i) + } + if !tr.registrationFails && err != nil { + t.Fatalf("registration with wrapping registry failed for collector #%d: %s", i, err) + } + } + wantMF := toMetricFamilies(s.output...) + gotMF, err := reg.Gather() + if s.gatherFails && err == nil { + t.Fatal("gathering unexpectedly succeded") + } + if !s.gatherFails && err != nil { + t.Fatal("gathering failed:", err) + } + if !reflect.DeepEqual(gotMF, wantMF) { + var want, got []string + + for i, mf := range wantMF { + want = append(want, fmt.Sprintf("%3d: %s", i, proto.MarshalTextString(mf))) + } + for i, mf := range gotMF { + got = append(got, fmt.Sprintf("%3d: %s", i, proto.MarshalTextString(mf))) + } + + t.Fatalf( + "unexpected output of gathering:\n\nWANT:\n%s\n\nGOT:\n%s\n", + strings.Join(want, "\n"), + strings.Join(got, "\n"), + ) + } + }) + } + +} diff --git a/vendor/go.opencensus.io/.gitignore b/vendor/go.opencensus.io/.gitignore new file mode 100644 index 0000000000..74a6db472e --- /dev/null +++ b/vendor/go.opencensus.io/.gitignore @@ -0,0 +1,9 @@ +/.idea/ + +# go.opencensus.io/exporter/aws +/exporter/aws/ + +# Exclude vendor, use dep ensure after checkout: +/vendor/github.com/ +/vendor/golang.org/ +/vendor/google.golang.org/ diff --git a/vendor/go.opencensus.io/AUTHORS b/vendor/go.opencensus.io/AUTHORS new file mode 100644 index 0000000000..e491a9e7f7 --- /dev/null +++ b/vendor/go.opencensus.io/AUTHORS @@ -0,0 +1 @@ +Google Inc. diff --git a/vendor/go.opencensus.io/CONTRIBUTING.md b/vendor/go.opencensus.io/CONTRIBUTING.md new file mode 100644 index 0000000000..3f3aed3961 --- /dev/null +++ b/vendor/go.opencensus.io/CONTRIBUTING.md @@ -0,0 +1,56 @@ +# How to contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution, +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult [GitHub Help] for more +information on using pull requests. + +[GitHub Help]: https://help.github.com/articles/about-pull-requests/ + +## Instructions + +Fork the repo, checkout the upstream repo to your GOPATH by: + +``` +$ go get -d go.opencensus.io +``` + +Add your fork as an origin: + +``` +cd $(go env GOPATH)/src/go.opencensus.io +git remote add fork git@github.com:YOUR_GITHUB_USERNAME/opencensus-go.git +``` + +Run tests: + +``` +$ go test ./... +``` + +Checkout a new branch, make modifications and push the branch to your fork: + +``` +$ git checkout -b feature +# edit files +$ git commit +$ git push fork feature +``` + +Open a pull request against the main opencensus-go repo. diff --git a/vendor/go.opencensus.io/Gopkg.lock b/vendor/go.opencensus.io/Gopkg.lock new file mode 100644 index 0000000000..3be12ac8f2 --- /dev/null +++ b/vendor/go.opencensus.io/Gopkg.lock @@ -0,0 +1,231 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + digest = "1:eee9386329f4fcdf8d6c0def0c9771b634bdd5ba460d888aa98c17d59b37a76c" + name = "git.apache.org/thrift.git" + packages = ["lib/go/thrift"] + pruneopts = "UT" + revision = "6e67faa92827ece022380b211c2caaadd6145bf5" + source = "github.com/apache/thrift" + +[[projects]] + branch = "master" + digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d" + name = "github.com/beorn7/perks" + packages = ["quantile"] + pruneopts = "UT" + revision = "3a771d992973f24aa725d07868b467d1ddfceafb" + +[[projects]] + digest = "1:4c0989ca0bcd10799064318923b9bc2db6b4d6338dd75f3f2d86c3511aaaf5cf" + name = "github.com/golang/protobuf" + packages = [ + "proto", + "ptypes", + "ptypes/any", + "ptypes/duration", + "ptypes/timestamp", + ] + pruneopts = "UT" + revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5" + version = "v1.2.0" + +[[projects]] + digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc" + name = "github.com/matttproud/golang_protobuf_extensions" + packages = ["pbutil"] + pruneopts = "UT" + revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" + version = "v1.0.1" + +[[projects]] + digest = "1:824c8f3aa4c5f23928fa84ebbd5ed2e9443b3f0cb958a40c1f2fbed5cf5e64b1" + name = "github.com/openzipkin/zipkin-go" + packages = [ + ".", + "idgenerator", + "model", + "propagation", + "reporter", + "reporter/http", + ] + pruneopts = "UT" + revision = "d455a5674050831c1e187644faa4046d653433c2" + version = "v0.1.1" + +[[projects]] + digest = "1:d14a5f4bfecf017cb780bdde1b6483e5deb87e12c332544d2c430eda58734bcb" + name = "github.com/prometheus/client_golang" + packages = [ + "prometheus", + "prometheus/promhttp", + ] + pruneopts = "UT" + revision = "c5b7fccd204277076155f10851dad72b76a49317" + version = "v0.8.0" + +[[projects]] + branch = "master" + digest = "1:2d5cd61daa5565187e1d96bae64dbbc6080dacf741448e9629c64fd93203b0d4" + name = "github.com/prometheus/client_model" + packages = ["go"] + pruneopts = "UT" + revision = "5c3871d89910bfb32f5fcab2aa4b9ec68e65a99f" + +[[projects]] + branch = "master" + digest = "1:63b68062b8968092eb86bedc4e68894bd096ea6b24920faca8b9dcf451f54bb5" + name = "github.com/prometheus/common" + packages = [ + "expfmt", + "internal/bitbucket.org/ww/goautoneg", + "model", + ] + pruneopts = "UT" + revision = "c7de2306084e37d54b8be01f3541a8464345e9a5" + +[[projects]] + branch = "master" + digest = "1:8c49953a1414305f2ff5465147ee576dd705487c35b15918fcd4efdc0cb7a290" + name = "github.com/prometheus/procfs" + packages = [ + ".", + "internal/util", + "nfs", + "xfs", + ] + pruneopts = "UT" + revision = "05ee40e3a273f7245e8777337fc7b46e533a9a92" + +[[projects]] + branch = "master" + digest = "1:deafe4ab271911fec7de5b693d7faae3f38796d9eb8622e2b9e7df42bb3dfea9" + name = "golang.org/x/net" + packages = [ + "context", + "http/httpguts", + "http2", + "http2/hpack", + "idna", + "internal/timeseries", + "trace", + ] + pruneopts = "UT" + revision = "922f4815f713f213882e8ef45e0d315b164d705c" + +[[projects]] + branch = "master" + digest = "1:e0140c0c868c6e0f01c0380865194592c011fe521d6e12d78bfd33e756fe018a" + name = "golang.org/x/sync" + packages = ["semaphore"] + pruneopts = "UT" + revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca" + +[[projects]] + branch = "master" + digest = "1:a3f00ac457c955fe86a41e1495e8f4c54cb5399d609374c5cc26aa7d72e542c8" + name = "golang.org/x/sys" + packages = ["unix"] + pruneopts = "UT" + revision = "3b58ed4ad3395d483fc92d5d14123ce2c3581fec" + +[[projects]] + digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" + name = "golang.org/x/text" + packages = [ + "collate", + "collate/build", + "internal/colltab", + "internal/gen", + "internal/tag", + "internal/triegen", + "internal/ucd", + "language", + "secure/bidirule", + "transform", + "unicode/bidi", + "unicode/cldr", + "unicode/norm", + "unicode/rangetable", + ] + pruneopts = "UT" + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" + +[[projects]] + branch = "master" + digest = "1:c0c17c94fe8bc1ab34e7f586a4a8b788c5e1f4f9f750ff23395b8b2f5a523530" + name = "google.golang.org/api" + packages = ["support/bundler"] + pruneopts = "UT" + revision = "e21acd801f91da814261b938941d193bb036441a" + +[[projects]] + branch = "master" + digest = "1:077c1c599507b3b3e9156d17d36e1e61928ee9b53a5b420f10f28ebd4a0b275c" + name = "google.golang.org/genproto" + packages = ["googleapis/rpc/status"] + pruneopts = "UT" + revision = "c66870c02cf823ceb633bcd05be3c7cda29976f4" + +[[projects]] + digest = "1:3dd7996ce6bf52dec6a2f69fa43e7c4cefea1d4dfa3c8ab7a5f8a9f7434e239d" + name = "google.golang.org/grpc" + packages = [ + ".", + "balancer", + "balancer/base", + "balancer/roundrobin", + "codes", + "connectivity", + "credentials", + "encoding", + "encoding/proto", + "grpclog", + "internal", + "internal/backoff", + "internal/channelz", + "internal/envconfig", + "internal/grpcrand", + "internal/transport", + "keepalive", + "metadata", + "naming", + "peer", + "resolver", + "resolver/dns", + "resolver/passthrough", + "stats", + "status", + "tap", + ] + pruneopts = "UT" + revision = "32fb0ac620c32ba40a4626ddf94d90d12cce3455" + version = "v1.14.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + input-imports = [ + "git.apache.org/thrift.git/lib/go/thrift", + "github.com/golang/protobuf/proto", + "github.com/openzipkin/zipkin-go", + "github.com/openzipkin/zipkin-go/model", + "github.com/openzipkin/zipkin-go/reporter", + "github.com/openzipkin/zipkin-go/reporter/http", + "github.com/prometheus/client_golang/prometheus", + "github.com/prometheus/client_golang/prometheus/promhttp", + "golang.org/x/net/context", + "golang.org/x/net/http2", + "google.golang.org/api/support/bundler", + "google.golang.org/grpc", + "google.golang.org/grpc/codes", + "google.golang.org/grpc/grpclog", + "google.golang.org/grpc/metadata", + "google.golang.org/grpc/stats", + "google.golang.org/grpc/status", + ] + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/vendor/go.opencensus.io/Gopkg.toml b/vendor/go.opencensus.io/Gopkg.toml new file mode 100644 index 0000000000..a9f3cd68eb --- /dev/null +++ b/vendor/go.opencensus.io/Gopkg.toml @@ -0,0 +1,36 @@ +# For v0.x.y dependencies, prefer adding a constraints of the form: version=">= 0.x.y" +# to avoid locking to a particular minor version which can cause dep to not be +# able to find a satisfying dependency graph. + +[[constraint]] + branch = "master" + name = "git.apache.org/thrift.git" + source = "github.com/apache/thrift" + +[[constraint]] + name = "github.com/golang/protobuf" + version = "1.0.0" + +[[constraint]] + name = "github.com/openzipkin/zipkin-go" + version = ">=0.1.0" + +[[constraint]] + name = "github.com/prometheus/client_golang" + version = ">=0.8.0" + +[[constraint]] + branch = "master" + name = "golang.org/x/net" + +[[constraint]] + branch = "master" + name = "google.golang.org/api" + +[[constraint]] + name = "google.golang.org/grpc" + version = "1.11.3" + +[prune] + go-tests = true + unused-packages = true diff --git a/vendor/go.opencensus.io/LICENSE b/vendor/go.opencensus.io/LICENSE new file mode 100644 index 0000000000..7a4a3ea242 --- /dev/null +++ b/vendor/go.opencensus.io/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/vendor/go.opencensus.io/README.md b/vendor/go.opencensus.io/README.md new file mode 100644 index 0000000000..97d66983d3 --- /dev/null +++ b/vendor/go.opencensus.io/README.md @@ -0,0 +1,263 @@ +# OpenCensus Libraries for Go + +[![Build Status][travis-image]][travis-url] +[![Windows Build Status][appveyor-image]][appveyor-url] +[![GoDoc][godoc-image]][godoc-url] +[![Gitter chat][gitter-image]][gitter-url] + +OpenCensus Go is a Go implementation of OpenCensus, a toolkit for +collecting application performance and behavior monitoring data. +Currently it consists of three major components: tags, stats and tracing. + +## Installation + +``` +$ go get -u go.opencensus.io +``` + +The API of this project is still evolving, see: [Deprecation Policy](#deprecation-policy). +The use of vendoring or a dependency management tool is recommended. + +## Prerequisites + +OpenCensus Go libraries require Go 1.8 or later. + +## Getting Started + +The easiest way to get started using OpenCensus in your application is to use an existing +integration with your RPC framework: + +* [net/http](https://godoc.org/go.opencensus.io/plugin/ochttp) +* [gRPC](https://godoc.org/go.opencensus.io/plugin/ocgrpc) +* [database/sql](https://godoc.org/github.com/basvanbeek/ocsql) +* [Go kit](https://godoc.org/github.com/go-kit/kit/tracing/opencensus) +* [Groupcache](https://godoc.org/github.com/orijtech/groupcache) +* [Caddy webserver](https://godoc.org/github.com/orijtech/caddy) +* [MongoDB](https://godoc.org/github.com/orijtech/mongo-go-driver) +* [Redis gomodule/redigo](https://godoc.org/github.com/orijtech/redigo) +* [Redis goredis/redis](https://godoc.org/github.com/orijtech/redis) +* [Memcache](https://godoc.org/github.com/orijtech/gomemcache) + +If you're using a framework not listed here, you could either implement your own middleware for your +framework or use [custom stats](#stats) and [spans](#spans) directly in your application. + +## Exporters + +OpenCensus can export instrumentation data to various backends. +OpenCensus has exporter implementations for the following, users +can implement their own exporters by implementing the exporter interfaces +([stats](https://godoc.org/go.opencensus.io/stats/view#Exporter), +[trace](https://godoc.org/go.opencensus.io/trace#Exporter)): + +* [Prometheus][exporter-prom] for stats +* [OpenZipkin][exporter-zipkin] for traces +* [Stackdriver][exporter-stackdriver] Monitoring for stats and Trace for traces +* [Jaeger][exporter-jaeger] for traces +* [AWS X-Ray][exporter-xray] for traces +* [Datadog][exporter-datadog] for stats and traces +* [Graphite][exporter-graphite] for stats +* [Honeycomb][exporter-honeycomb] for traces + +## Overview + +![OpenCensus Overview](https://i.imgur.com/cf4ElHE.jpg) + +In a microservices environment, a user request may go through +multiple services until there is a response. OpenCensus allows +you to instrument your services and collect diagnostics data all +through your services end-to-end. + +## Tags + +Tags represent propagated key-value pairs. They are propagated using `context.Context` +in the same process or can be encoded to be transmitted on the wire. Usually, this will +be handled by an integration plugin, e.g. `ocgrpc.ServerHandler` and `ocgrpc.ClientHandler` +for gRPC. + +Package `tag` allows adding or modifying tags in the current context. + +[embedmd]:# (internal/readme/tags.go new) +```go +ctx, err = tag.New(ctx, + tag.Insert(osKey, "macOS-10.12.5"), + tag.Upsert(userIDKey, "cde36753ed"), +) +if err != nil { + log.Fatal(err) +} +``` + +## Stats + +OpenCensus is a low-overhead framework even if instrumentation is always enabled. +In order to be so, it is optimized to make recording of data points fast +and separate from the data aggregation. + +OpenCensus stats collection happens in two stages: + +* Definition of measures and recording of data points +* Definition of views and aggregation of the recorded data + +### Recording + +Measurements are data points associated with a measure. +Recording implicitly tags the set of Measurements with the tags from the +provided context: + +[embedmd]:# (internal/readme/stats.go record) +```go +stats.Record(ctx, videoSize.M(102478)) +``` + +### Views + +Views are how Measures are aggregated. You can think of them as queries over the +set of recorded data points (measurements). + +Views have two parts: the tags to group by and the aggregation type used. + +Currently three types of aggregations are supported: +* CountAggregation is used to count the number of times a sample was recorded. +* DistributionAggregation is used to provide a histogram of the values of the samples. +* SumAggregation is used to sum up all sample values. + +[embedmd]:# (internal/readme/stats.go aggs) +```go +distAgg := view.Distribution(0, 1<<32, 2<<32, 3<<32) +countAgg := view.Count() +sumAgg := view.Sum() +``` + +Here we create a view with the DistributionAggregation over our measure. + +[embedmd]:# (internal/readme/stats.go view) +```go +if err := view.Register(&view.View{ + Name: "example.com/video_size_distribution", + Description: "distribution of processed video size over time", + Measure: videoSize, + Aggregation: view.Distribution(0, 1<<32, 2<<32, 3<<32), +}); err != nil { + log.Fatalf("Failed to register view: %v", err) +} +``` + +Register begins collecting data for the view. Registered views' data will be +exported via the registered exporters. + +## Traces + +A distributed trace tracks the progression of a single user request as +it is handled by the services and processes that make up an application. +Each step is called a span in the trace. Spans include metadata about the step, +including especially the time spent in the step, called the span’s latency. + +Below you see a trace and several spans underneath it. + +![Traces and spans](https://i.imgur.com/7hZwRVj.png) + +### Spans + +Span is the unit step in a trace. Each span has a name, latency, status and +additional metadata. + +Below we are starting a span for a cache read and ending it +when we are done: + +[embedmd]:# (internal/readme/trace.go startend) +```go +ctx, span := trace.StartSpan(ctx, "cache.Get") +defer span.End() + +// Do work to get from cache. +``` + +### Propagation + +Spans can have parents or can be root spans if they don't have any parents. +The current span is propagated in-process and across the network to allow associating +new child spans with the parent. + +In the same process, `context.Context` is used to propagate spans. +`trace.StartSpan` creates a new span as a root if the current context +doesn't contain a span. Or, it creates a child of the span that is +already in current context. The returned context can be used to keep +propagating the newly created span in the current context. + +[embedmd]:# (internal/readme/trace.go startend) +```go +ctx, span := trace.StartSpan(ctx, "cache.Get") +defer span.End() + +// Do work to get from cache. +``` + +Across the network, OpenCensus provides different propagation +methods for different protocols. + +* gRPC integrations use the OpenCensus' [binary propagation format](https://godoc.org/go.opencensus.io/trace/propagation). +* HTTP integrations use Zipkin's [B3](https://github.com/openzipkin/b3-propagation) + by default but can be configured to use a custom propagation method by setting another + [propagation.HTTPFormat](https://godoc.org/go.opencensus.io/trace/propagation#HTTPFormat). + +## Execution Tracer + +With Go 1.11, OpenCensus Go will support integration with the Go execution tracer. +See [Debugging Latency in Go](https://medium.com/observability/debugging-latency-in-go-1-11-9f97a7910d68) +for an example of their mutual use. + +## Profiles + +OpenCensus tags can be applied as profiler labels +for users who are on Go 1.9 and above. + +[embedmd]:# (internal/readme/tags.go profiler) +```go +ctx, err = tag.New(ctx, + tag.Insert(osKey, "macOS-10.12.5"), + tag.Insert(userIDKey, "fff0989878"), +) +if err != nil { + log.Fatal(err) +} +tag.Do(ctx, func(ctx context.Context) { + // Do work. + // When profiling is on, samples will be + // recorded with the key/values from the tag map. +}) +``` + +A screenshot of the CPU profile from the program above: + +![CPU profile](https://i.imgur.com/jBKjlkw.png) + +## Deprecation Policy + +Before version 1.0.0, the following deprecation policy will be observed: + +No backwards-incompatible changes will be made except for the removal of symbols that have +been marked as *Deprecated* for at least one minor release (e.g. 0.9.0 to 0.10.0). A release +removing the *Deprecated* functionality will be made no sooner than 28 days after the first +release in which the functionality was marked *Deprecated*. + +[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-go.svg?branch=master +[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-go +[appveyor-image]: https://ci.appveyor.com/api/projects/status/vgtt29ps1783ig38?svg=true +[appveyor-url]: https://ci.appveyor.com/project/opencensusgoteam/opencensus-go/branch/master +[godoc-image]: https://godoc.org/go.opencensus.io?status.svg +[godoc-url]: https://godoc.org/go.opencensus.io +[gitter-image]: https://badges.gitter.im/census-instrumentation/lobby.svg +[gitter-url]: https://gitter.im/census-instrumentation/lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge + + +[new-ex]: https://godoc.org/go.opencensus.io/tag#example-NewMap +[new-replace-ex]: https://godoc.org/go.opencensus.io/tag#example-NewMap--Replace + +[exporter-prom]: https://godoc.org/go.opencensus.io/exporter/prometheus +[exporter-stackdriver]: https://godoc.org/contrib.go.opencensus.io/exporter/stackdriver +[exporter-zipkin]: https://godoc.org/go.opencensus.io/exporter/zipkin +[exporter-jaeger]: https://godoc.org/go.opencensus.io/exporter/jaeger +[exporter-xray]: https://github.com/census-ecosystem/opencensus-go-exporter-aws +[exporter-datadog]: https://github.com/DataDog/opencensus-go-exporter-datadog +[exporter-graphite]: https://github.com/census-ecosystem/opencensus-go-exporter-graphite +[exporter-honeycomb]: https://github.com/honeycombio/opencensus-exporter diff --git a/vendor/go.opencensus.io/appveyor.yml b/vendor/go.opencensus.io/appveyor.yml new file mode 100644 index 0000000000..98057888a3 --- /dev/null +++ b/vendor/go.opencensus.io/appveyor.yml @@ -0,0 +1,24 @@ +version: "{build}" + +platform: x64 + +clone_folder: c:\gopath\src\go.opencensus.io + +environment: + GOPATH: 'c:\gopath' + GOVERSION: '1.11' + GO111MODULE: 'on' + CGO_ENABLED: '0' # See: https://github.com/appveyor/ci/issues/2613 + +install: + - set PATH=%GOPATH%\bin;c:\go\bin;%PATH% + - go version + - go env + +build: false +deploy: false + +test_script: + - cd %APPVEYOR_BUILD_FOLDER% + - go build -v .\... + - go test -v .\... # No -race because cgo is disabled diff --git a/vendor/go.opencensus.io/examples/exporter/exporter.go b/vendor/go.opencensus.io/examples/exporter/exporter.go new file mode 100644 index 0000000000..43bd73a66b --- /dev/null +++ b/vendor/go.opencensus.io/examples/exporter/exporter.go @@ -0,0 +1,105 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package exporter // import "go.opencensus.io/examples/exporter" + +import ( + "encoding/hex" + "fmt" + "regexp" + "time" + + "go.opencensus.io/stats/view" + "go.opencensus.io/trace" +) + +// indent these many spaces +const indent = " " + +// reZero provides a simple way to detect an empty ID +var reZero = regexp.MustCompile(`^0+$`) + +// PrintExporter is a stats and trace exporter that logs +// the exported data to the console. +// +// The intent is help new users familiarize themselves with the +// capabilities of opencensus. +// +// This should NOT be used for production workloads. +type PrintExporter struct{} + +// ExportView logs the view data. +func (e *PrintExporter) ExportView(vd *view.Data) { + for _, row := range vd.Rows { + fmt.Printf("%v %-45s", vd.End.Format("15:04:05"), vd.View.Name) + + switch v := row.Data.(type) { + case *view.DistributionData: + fmt.Printf("distribution: min=%.1f max=%.1f mean=%.1f", v.Min, v.Max, v.Mean) + case *view.CountData: + fmt.Printf("count: value=%v", v.Value) + case *view.SumData: + fmt.Printf("sum: value=%v", v.Value) + case *view.LastValueData: + fmt.Printf("last: value=%v", v.Value) + } + fmt.Println() + + for _, tag := range row.Tags { + fmt.Printf("%v- %v=%v\n", indent, tag.Key.Name(), tag.Value) + } + } +} + +// ExportSpan logs the trace span. +func (e *PrintExporter) ExportSpan(vd *trace.SpanData) { + var ( + traceID = hex.EncodeToString(vd.SpanContext.TraceID[:]) + spanID = hex.EncodeToString(vd.SpanContext.SpanID[:]) + parentSpanID = hex.EncodeToString(vd.ParentSpanID[:]) + ) + fmt.Println() + fmt.Println("#----------------------------------------------") + fmt.Println() + fmt.Println("TraceID: ", traceID) + fmt.Println("SpanID: ", spanID) + if !reZero.MatchString(parentSpanID) { + fmt.Println("ParentSpanID:", parentSpanID) + } + + fmt.Println() + fmt.Printf("Span: %v\n", vd.Name) + fmt.Printf("Status: %v [%v]\n", vd.Status.Message, vd.Status.Code) + fmt.Printf("Elapsed: %v\n", vd.EndTime.Sub(vd.StartTime).Round(time.Millisecond)) + + if len(vd.Annotations) > 0 { + fmt.Println() + fmt.Println("Annotations:") + for _, item := range vd.Annotations { + fmt.Print(indent, item.Message) + for k, v := range item.Attributes { + fmt.Printf(" %v=%v", k, v) + } + fmt.Println() + } + } + + if len(vd.Attributes) > 0 { + fmt.Println() + fmt.Println("Attributes:") + for k, v := range vd.Attributes { + fmt.Printf("%v- %v=%v\n", indent, k, v) + } + } +} diff --git a/vendor/go.opencensus.io/examples/grpc/README.md b/vendor/go.opencensus.io/examples/grpc/README.md new file mode 100644 index 0000000000..69ba2d9951 --- /dev/null +++ b/vendor/go.opencensus.io/examples/grpc/README.md @@ -0,0 +1,31 @@ +# Example gRPC server and client with OpenCensus + +This example uses: + +* gRPC to create an RPC server and client. +* The OpenCensus gRPC plugin to instrument the RPC server and client. +* Debugging exporters to print stats and traces to stdout. + +``` +$ go get go.opencensus.io/examples/grpc/... +``` + +First, run the server: + +``` +$ go run $(go env GOPATH)/src/go.opencensus.io/examples/grpc/helloworld_server/main.go +``` + +Then, run the client: + +``` +$ go run $(go env GOPATH)/src/go.opencensus.io/examples/grpc/helloworld_client/main.go +``` + +You will see traces and stats exported on the stdout. You can use one of the +[exporters](https://godoc.org/go.opencensus.io/exporter) +to upload collected data to the backend of your choice. + +You can also see the z-pages provided from the server: +* Traces: http://localhost:8081/debug/tracez +* RPCs: http://localhost:8081/debug/rpcz diff --git a/vendor/go.opencensus.io/examples/grpc/helloworld_client/main.go b/vendor/go.opencensus.io/examples/grpc/helloworld_client/main.go new file mode 100644 index 0000000000..12d845090a --- /dev/null +++ b/vendor/go.opencensus.io/examples/grpc/helloworld_client/main.go @@ -0,0 +1,69 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "log" + "os" + "time" + + "go.opencensus.io/examples/exporter" + pb "go.opencensus.io/examples/grpc/proto" + "go.opencensus.io/plugin/ocgrpc" + "go.opencensus.io/stats/view" + "golang.org/x/net/context" + "google.golang.org/grpc" +) + +const ( + address = "localhost:50051" + defaultName = "world" +) + +func main() { + // Register stats and trace exporters to export + // the collected data. + view.RegisterExporter(&exporter.PrintExporter{}) + + // Register the view to collect gRPC client stats. + if err := view.Register(ocgrpc.DefaultClientViews...); err != nil { + log.Fatal(err) + } + + // Set up a connection to the server with the OpenCensus + // stats handler to enable stats and tracing. + conn, err := grpc.Dial(address, grpc.WithStatsHandler(&ocgrpc.ClientHandler{}), grpc.WithInsecure()) + if err != nil { + log.Fatalf("Cannot connect: %v", err) + } + defer conn.Close() + c := pb.NewGreeterClient(conn) + + // Contact the server and print out its response. + name := defaultName + if len(os.Args) > 1 { + name = os.Args[1] + } + view.SetReportingPeriod(time.Second) + for { + r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name}) + if err != nil { + log.Printf("Could not greet: %v", err) + } else { + log.Printf("Greeting: %s", r.Message) + } + time.Sleep(2 * time.Second) + } +} diff --git a/vendor/go.opencensus.io/examples/grpc/helloworld_server/main.go b/vendor/go.opencensus.io/examples/grpc/helloworld_server/main.go new file mode 100644 index 0000000000..c0215a9212 --- /dev/null +++ b/vendor/go.opencensus.io/examples/grpc/helloworld_server/main.go @@ -0,0 +1,79 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:generate protoc -I ../proto --go_out=plugins=grpc:../proto ../proto/helloworld.proto + +package main + +import ( + "log" + "math/rand" + "net" + "net/http" + "time" + + "go.opencensus.io/examples/exporter" + pb "go.opencensus.io/examples/grpc/proto" + "go.opencensus.io/plugin/ocgrpc" + "go.opencensus.io/stats/view" + "go.opencensus.io/trace" + "go.opencensus.io/zpages" + "golang.org/x/net/context" + "google.golang.org/grpc" +) + +const port = ":50051" + +// server is used to implement helloworld.GreeterServer. +type server struct{} + +// SayHello implements helloworld.GreeterServer +func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { + ctx, span := trace.StartSpan(ctx, "sleep") + time.Sleep(time.Duration(rand.Float64() * float64(time.Second))) + span.End() + return &pb.HelloReply{Message: "Hello " + in.Name}, nil +} + +func main() { + // Start z-Pages server. + go func() { + mux := http.NewServeMux() + zpages.Handle(mux, "/debug") + log.Fatal(http.ListenAndServe("127.0.0.1:8081", mux)) + }() + + // Register stats and trace exporters to export + // the collected data. + view.RegisterExporter(&exporter.PrintExporter{}) + + // Register the views to collect server request count. + if err := view.Register(ocgrpc.DefaultServerViews...); err != nil { + log.Fatal(err) + } + + lis, err := net.Listen("tcp", port) + if err != nil { + log.Fatalf("Failed to listen: %v", err) + } + + // Set up a new server with the OpenCensus + // stats handler to enable stats and tracing. + s := grpc.NewServer(grpc.StatsHandler(&ocgrpc.ServerHandler{})) + pb.RegisterGreeterServer(s, &server{}) + + if err := s.Serve(lis); err != nil { + log.Fatalf("Failed to serve: %v", err) + } +} diff --git a/vendor/go.opencensus.io/examples/grpc/proto/helloworld.pb.go b/vendor/go.opencensus.io/examples/grpc/proto/helloworld.pb.go new file mode 100644 index 0000000000..23bbc5dfbf --- /dev/null +++ b/vendor/go.opencensus.io/examples/grpc/proto/helloworld.pb.go @@ -0,0 +1,164 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: helloworld.proto + +/* +Package helloworld is a generated protocol buffer package. + +It is generated from these files: + helloworld.proto + +It has these top-level messages: + HelloRequest + HelloReply +*/ +package helloworld // import "go.opencensus.io/examples/grpc/proto" + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// The request message containing the user's name. +type HelloRequest struct { + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` +} + +func (m *HelloRequest) Reset() { *m = HelloRequest{} } +func (m *HelloRequest) String() string { return proto.CompactTextString(m) } +func (*HelloRequest) ProtoMessage() {} +func (*HelloRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *HelloRequest) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +// The response message containing the greetings +type HelloReply struct { + Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"` +} + +func (m *HelloReply) Reset() { *m = HelloReply{} } +func (m *HelloReply) String() string { return proto.CompactTextString(m) } +func (*HelloReply) ProtoMessage() {} +func (*HelloReply) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } + +func (m *HelloReply) GetMessage() string { + if m != nil { + return m.Message + } + return "" +} + +func init() { + proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest") + proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply") +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// Client API for Greeter service + +type GreeterClient interface { + // Sends a greeting + SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) +} + +type greeterClient struct { + cc *grpc.ClientConn +} + +func NewGreeterClient(cc *grpc.ClientConn) GreeterClient { + return &greeterClient{cc} +} + +func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { + out := new(HelloReply) + err := grpc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for Greeter service + +type GreeterServer interface { + // Sends a greeting + SayHello(context.Context, *HelloRequest) (*HelloReply, error) +} + +func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { + s.RegisterService(&_Greeter_serviceDesc, srv) +} + +func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GreeterServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/helloworld.Greeter/SayHello", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Greeter_serviceDesc = grpc.ServiceDesc{ + ServiceName: "helloworld.Greeter", + HandlerType: (*GreeterServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _Greeter_SayHello_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "helloworld.proto", +} + +func init() { proto.RegisterFile("helloworld.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 175 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xc8, 0x48, 0xcd, 0xc9, + 0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x88, + 0x28, 0x29, 0x71, 0xf1, 0x78, 0x80, 0x78, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x42, + 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x92, + 0x1a, 0x17, 0x17, 0x54, 0x4d, 0x41, 0x4e, 0xa5, 0x90, 0x04, 0x17, 0x7b, 0x6e, 0x6a, 0x71, 0x71, + 0x62, 0x3a, 0x4c, 0x11, 0x8c, 0x6b, 0xe4, 0xc9, 0xc5, 0xee, 0x5e, 0x94, 0x9a, 0x5a, 0x92, 0x5a, + 0x24, 0x64, 0xc7, 0xc5, 0x11, 0x9c, 0x58, 0x09, 0xd6, 0x25, 0x24, 0xa1, 0x87, 0xe4, 0x02, 0x64, + 0xcb, 0xa4, 0xc4, 0xb0, 0xc8, 0x14, 0xe4, 0x54, 0x2a, 0x31, 0x38, 0x19, 0x70, 0x49, 0x67, 0xe6, + 0xeb, 0xa5, 0x17, 0x15, 0x24, 0xeb, 0xa5, 0x56, 0x24, 0xe6, 0x16, 0xe4, 0xa4, 0x16, 0x23, 0xa9, + 0x75, 0xe2, 0x07, 0x2b, 0x0e, 0x07, 0xb1, 0x03, 0x40, 0x5e, 0x0a, 0x60, 0x4c, 0x62, 0x03, 0xfb, + 0xcd, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x0f, 0xb7, 0xcd, 0xf2, 0xef, 0x00, 0x00, 0x00, +} diff --git a/vendor/go.opencensus.io/examples/grpc/proto/helloworld.proto b/vendor/go.opencensus.io/examples/grpc/proto/helloworld.proto new file mode 100644 index 0000000000..e08f1f57f7 --- /dev/null +++ b/vendor/go.opencensus.io/examples/grpc/proto/helloworld.proto @@ -0,0 +1,37 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/vendor/go.opencensus.io/examples/helloworld/main.go b/vendor/go.opencensus.io/examples/helloworld/main.go new file mode 100644 index 0000000000..c93edcf29a --- /dev/null +++ b/vendor/go.opencensus.io/examples/helloworld/main.go @@ -0,0 +1,99 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Command helloworld is an example program that collects data for +// video size. +package main + +import ( + "context" + "fmt" + "log" + "math/rand" + "time" + + "go.opencensus.io/examples/exporter" + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" + "go.opencensus.io/trace" +) + +var ( + // frontendKey allows us to breakdown the recorded data + // by the frontend used when uploading the video. + frontendKey tag.Key + + // videoSize will measure the size of processed videos. + videoSize *stats.Int64Measure +) + +func main() { + ctx := context.Background() + + // Register an exporter to be able to retrieve + // the data from the subscribed views. + e := &exporter.PrintExporter{} + view.RegisterExporter(e) + trace.RegisterExporter(e) + + var err error + frontendKey, err = tag.NewKey("example.com/keys/frontend") + if err != nil { + log.Fatal(err) + } + videoSize = stats.Int64("example.com/measure/video_size", "size of processed videos", stats.UnitBytes) + view.SetReportingPeriod(2 * time.Second) + + // Create view to see the processed video size + // distribution broken down by frontend. + // Register will allow view data to be exported. + if err := view.Register(&view.View{ + Name: "example.com/views/video_size", + Description: "processed video size over time", + TagKeys: []tag.Key{frontendKey}, + Measure: videoSize, + Aggregation: view.Distribution(0, 1<<16, 1<<32), + }); err != nil { + log.Fatalf("Cannot register view: %v", err) + } + + // Process the video. + process(ctx) + + // Wait for a duration longer than reporting duration to ensure the stats + // library reports the collected data. + fmt.Println("Wait longer than the reporting duration...") + time.Sleep(2 * time.Second) +} + +// process processes the video and instruments the processing +// by creating a span and collecting metrics about the operation. +func process(ctx context.Context) { + ctx, err := tag.New(ctx, + tag.Insert(frontendKey, "mobile-ios9.3.5"), + ) + if err != nil { + log.Fatal(err) + } + ctx, span := trace.StartSpan(ctx, "example.com/ProcessVideo") + defer span.End() + // Process video. + // Record the processed video size. + + // Sleep for [1,10] milliseconds to fake work. + time.Sleep(time.Duration(rand.Intn(10)+1) * time.Millisecond) + + stats.Record(ctx, videoSize.M(25648)) +} diff --git a/vendor/go.opencensus.io/examples/http/README.md b/vendor/go.opencensus.io/examples/http/README.md new file mode 100644 index 0000000000..577fd31602 --- /dev/null +++ b/vendor/go.opencensus.io/examples/http/README.md @@ -0,0 +1,31 @@ +# Example net/http server and client with OpenCensus + +This example uses: + +* net/http to create a server and client. +* The OpenCensus net/http plugin to instrument the server and client. +* Debugging exporters to print stats and traces to stdout. + +``` +$ go get go.opencensus.io/examples/http/... +``` + +First, run the server: + +``` +$ go run $(go env GOPATH)/src/go.opencensus.io/examples/http/helloworld_server/main.go +``` + +Then, run the client: + +``` +$ go run $(go env GOPATH)/src/go.opencensus.io/examples/http/helloworld_client/main.go +``` + +You will see traces and stats exported on the stdout. You can use one of the +[exporters](https://godoc.org/go.opencensus.io/exporter) +to upload collected data to the backend of your choice. + +You can also see the z-pages provided from the server: +* Traces: http://localhost:8081/debug/tracez +* RPCs: http://localhost:8081/debug/rpcz diff --git a/vendor/go.opencensus.io/examples/http/helloworld_client/main.go b/vendor/go.opencensus.io/examples/http/helloworld_client/main.go new file mode 100644 index 0000000000..0a07536597 --- /dev/null +++ b/vendor/go.opencensus.io/examples/http/helloworld_client/main.go @@ -0,0 +1,55 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "log" + "net/http" + "time" + + "go.opencensus.io/plugin/ochttp" + "go.opencensus.io/trace" + + "go.opencensus.io/examples/exporter" + "go.opencensus.io/stats/view" +) + +const server = "http://localhost:50030" + +func main() { + // Register stats and trace exporters to export the collected data. + exporter := &exporter.PrintExporter{} + view.RegisterExporter(exporter) + trace.RegisterExporter(exporter) + + // Always trace for this demo. In a production application, you should + // configure this to a trace.ProbabilitySampler set at the desired + // probability. + trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) + + // Report stats at every second. + view.SetReportingPeriod(1 * time.Second) + + client := &http.Client{Transport: &ochttp.Transport{}} + + resp, err := client.Get(server) + if err != nil { + log.Printf("Failed to get response: %v", err) + } else { + resp.Body.Close() + } + + time.Sleep(2 * time.Second) // Wait until stats are reported. +} diff --git a/vendor/go.opencensus.io/examples/http/helloworld_server/main.go b/vendor/go.opencensus.io/examples/http/helloworld_server/main.go new file mode 100644 index 0000000000..16ed552618 --- /dev/null +++ b/vendor/go.opencensus.io/examples/http/helloworld_server/main.go @@ -0,0 +1,77 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "net/http" + "time" + + "go.opencensus.io/zpages" + + "go.opencensus.io/examples/exporter" + "go.opencensus.io/plugin/ochttp" + "go.opencensus.io/stats/view" + "go.opencensus.io/trace" +) + +func main() { + // Start z-Pages server. + go func() { + mux := http.NewServeMux() + zpages.Handle(mux, "/debug") + log.Fatal(http.ListenAndServe("127.0.0.1:8081", mux)) + }() + + // Register stats and trace exporters to export the collected data. + exporter := &exporter.PrintExporter{} + view.RegisterExporter(exporter) + trace.RegisterExporter(exporter) + + // Always trace for this demo. In a production application, you should + // configure this to a trace.ProbabilitySampler set at the desired + // probability. + trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) + + // Report stats at every second. + view.SetReportingPeriod(1 * time.Second) + + client := &http.Client{Transport: &ochttp.Transport{}} + + http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "hello world") + + // Provide an example of how spans can be annotated with metadata + _, span := trace.StartSpan(req.Context(), "child") + defer span.End() + span.Annotate([]trace.Attribute{trace.StringAttribute("key", "value")}, "something happened") + span.AddAttributes(trace.StringAttribute("hello", "world")) + time.Sleep(time.Millisecond * 125) + + r, _ := http.NewRequest("GET", "https://example.com", nil) + + // Propagate the trace header info in the outgoing requests. + r = r.WithContext(req.Context()) + resp, err := client.Do(r) + if err != nil { + log.Println(err) + } else { + // TODO: handle response + resp.Body.Close() + } + }) + log.Fatal(http.ListenAndServe(":50030", &ochttp.Handler{})) +} diff --git a/vendor/go.opencensus.io/examples/quickstart/stats.go b/vendor/go.opencensus.io/examples/quickstart/stats.go new file mode 100644 index 0000000000..19ddd48690 --- /dev/null +++ b/vendor/go.opencensus.io/examples/quickstart/stats.go @@ -0,0 +1,164 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Command stats implements the stats Quick Start example from: +// https://opencensus.io/quickstart/go/metrics/ +package main + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "log" + "os" + "time" + + "net/http" + + "go.opencensus.io/exporter/prometheus" + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" + "go.opencensus.io/zpages" +) + +var ( + // The latency in milliseconds + MLatencyMs = stats.Float64("repl/latency", "The latency in milliseconds per REPL loop", "ms") + + // Counts the number of lines read in from standard input + MLinesIn = stats.Int64("repl/lines_in", "The number of lines read in", "1") + + // Encounters the number of non EOF(end-of-file) errors. + MErrors = stats.Int64("repl/errors", "The number of errors encountered", "1") + + // Counts/groups the lengths of lines read in. + MLineLengths = stats.Int64("repl/line_lengths", "The distribution of line lengths", "By") +) + +var ( + KeyMethod, _ = tag.NewKey("method") +) + +var ( + LatencyView = &view.View{ + Name: "demo/latency", + Measure: MLatencyMs, + Description: "The distribution of the latencies", + + // Latency in buckets: + // [>=0ms, >=25ms, >=50ms, >=75ms, >=100ms, >=200ms, >=400ms, >=600ms, >=800ms, >=1s, >=2s, >=4s, >=6s] + Aggregation: view.Distribution(0, 25, 50, 75, 100, 200, 400, 600, 800, 1000, 2000, 4000, 6000), + TagKeys: []tag.Key{KeyMethod}} + + LineCountView = &view.View{ + Name: "demo/lines_in", + Measure: MLinesIn, + Description: "The number of lines from standard input", + Aggregation: view.Count(), + } + + ErrorCountView = &view.View{ + Name: "demo/errors", + Measure: MErrors, + Description: "The number of errors encountered", + Aggregation: view.Count(), + } + + LineLengthView = &view.View{ + Name: "demo/line_lengths", + Description: "Groups the lengths of keys in buckets", + Measure: MLineLengths, + // Lengths: [>=0B, >=5B, >=10B, >=15B, >=20B, >=40B, >=60B, >=80, >=100B, >=200B, >=400, >=600, >=800, >=1000] + Aggregation: view.Distribution(0, 5, 10, 15, 20, 40, 60, 80, 100, 200, 400, 600, 800, 1000), + } +) + +func main() { + zpages.Handle(nil, "/debug") + go http.ListenAndServe("localhost:8080", nil) + + // Create that Stackdriver stats exporter + exporter, err := prometheus.NewExporter(prometheus.Options{}) + if err != nil { + log.Fatalf("Failed to create the Stackdriver stats exporter: %v", err) + } + http.Handle("/metrics", exporter) + + // Register the stats exporter + view.RegisterExporter(exporter) + + // Register the views + if err := view.Register(LatencyView, LineCountView, ErrorCountView, LineLengthView); err != nil { + log.Fatalf("Failed to register views: %v", err) + } + + // But also we can change the metrics reporting period to 2 seconds + //view.SetReportingPeriod(2 * time.Second) + + // In a REPL: + // 1. Read input + // 2. process input + br := bufio.NewReader(os.Stdin) + + // repl is the read, evaluate, print, loop + for { + if err := readEvaluateProcess(br); err != nil { + if err == io.EOF { + return + } + log.Fatal(err) + } + } +} + +// readEvaluateProcess reads a line from the input reader and +// then processes it. It returns an error if any was encountered. +func readEvaluateProcess(br *bufio.Reader) error { + ctx, err := tag.New(context.Background(), tag.Insert(KeyMethod, "repl")) + if err != nil { + return err + } + + fmt.Printf("> ") + line, _, err := br.ReadLine() + if err != nil { + if err != io.EOF { + stats.Record(ctx, MErrors.M(1)) + } + return err + } + + out, err := processLine(ctx, line) + if err != nil { + stats.Record(ctx, MErrors.M(1)) + return err + } + fmt.Printf("< %s\n\n", out) + return nil +} + +// processLine takes in a line of text and +// transforms it. Currently it just capitalizes it. +func processLine(ctx context.Context, in []byte) (out []byte, err error) { + startTime := time.Now() + defer func() { + ms := float64(time.Since(startTime).Nanoseconds()) / 1e6 + stats.Record(ctx, MLinesIn.M(1), MLatencyMs.M(ms), MLineLengths.M(int64(len(in)))) + }() + + return bytes.ToUpper(in), nil +} diff --git a/vendor/go.opencensus.io/exemplar/exemplar.go b/vendor/go.opencensus.io/exemplar/exemplar.go new file mode 100644 index 0000000000..e676df837f --- /dev/null +++ b/vendor/go.opencensus.io/exemplar/exemplar.go @@ -0,0 +1,78 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package exemplar implements support for exemplars. Exemplars are additional +// data associated with each measurement. +// +// Their purpose it to provide an example of the kind of thing +// (request, RPC, trace span, etc.) that resulted in that measurement. +package exemplar + +import ( + "context" + "time" +) + +const ( + KeyTraceID = "trace_id" + KeySpanID = "span_id" + KeyPrefixTag = "tag:" +) + +// Exemplar is an example data point associated with each bucket of a +// distribution type aggregation. +type Exemplar struct { + Value float64 // the value that was recorded + Timestamp time.Time // the time the value was recorded + Attachments Attachments // attachments (if any) +} + +// Attachments is a map of extra values associated with a recorded data point. +// The map should only be mutated from AttachmentExtractor functions. +type Attachments map[string]string + +// AttachmentExtractor is a function capable of extracting exemplar attachments +// from the context used to record measurements. +// The map passed to the function should be mutated and returned. It will +// initially be nil: the first AttachmentExtractor that would like to add keys to the +// map is responsible for initializing it. +type AttachmentExtractor func(ctx context.Context, a Attachments) Attachments + +var extractors []AttachmentExtractor + +// RegisterAttachmentExtractor registers the given extractor associated with the exemplar +// type name. +// +// Extractors will be used to attempt to extract exemplars from the context +// associated with each recorded measurement. +// +// Packages that support exemplars should register their extractor functions on +// initialization. +// +// RegisterAttachmentExtractor should not be called after any measurements have +// been recorded. +func RegisterAttachmentExtractor(e AttachmentExtractor) { + extractors = append(extractors, e) +} + +// NewFromContext extracts exemplars from the given context. +// Each registered AttachmentExtractor (see RegisterAttachmentExtractor) is called in an +// unspecified order to add attachments to the exemplar. +func AttachmentsFromContext(ctx context.Context) Attachments { + var a Attachments + for _, extractor := range extractors { + a = extractor(ctx, a) + } + return a +} diff --git a/vendor/go.opencensus.io/exporter/jaeger/agent.go b/vendor/go.opencensus.io/exporter/jaeger/agent.go new file mode 100644 index 0000000000..362a571a0b --- /dev/null +++ b/vendor/go.opencensus.io/exporter/jaeger/agent.go @@ -0,0 +1,89 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jaeger + +import ( + "fmt" + "io" + "net" + + "git.apache.org/thrift.git/lib/go/thrift" + gen "go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger" +) + +// udpPacketMaxLength is the max size of UDP packet we want to send, synced with jaeger-agent +const udpPacketMaxLength = 65000 + +// agentClientUDP is a UDP client to Jaeger agent that implements gen.Agent interface. +type agentClientUDP struct { + gen.Agent + io.Closer + + connUDP *net.UDPConn + client *gen.AgentClient + maxPacketSize int // max size of datagram in bytes + thriftBuffer *thrift.TMemoryBuffer // buffer used to calculate byte size of a span +} + +// newAgentClientUDP creates a client that sends spans to Jaeger Agent over UDP. +func newAgentClientUDP(hostPort string, maxPacketSize int) (*agentClientUDP, error) { + if maxPacketSize == 0 { + maxPacketSize = udpPacketMaxLength + } + + thriftBuffer := thrift.NewTMemoryBufferLen(maxPacketSize) + protocolFactory := thrift.NewTCompactProtocolFactory() + client := gen.NewAgentClientFactory(thriftBuffer, protocolFactory) + + destAddr, err := net.ResolveUDPAddr("udp", hostPort) + if err != nil { + return nil, err + } + + connUDP, err := net.DialUDP(destAddr.Network(), nil, destAddr) + if err != nil { + return nil, err + } + if err := connUDP.SetWriteBuffer(maxPacketSize); err != nil { + return nil, err + } + + clientUDP := &agentClientUDP{ + connUDP: connUDP, + client: client, + maxPacketSize: maxPacketSize, + thriftBuffer: thriftBuffer} + return clientUDP, nil +} + +// EmitBatch implements EmitBatch() of Agent interface +func (a *agentClientUDP) EmitBatch(batch *gen.Batch) error { + a.thriftBuffer.Reset() + a.client.SeqId = 0 // we have no need for distinct SeqIds for our one-way UDP messages + if err := a.client.EmitBatch(batch); err != nil { + return err + } + if a.thriftBuffer.Len() > a.maxPacketSize { + return fmt.Errorf("Data does not fit within one UDP packet; size %d, max %d, spans %d", + a.thriftBuffer.Len(), a.maxPacketSize, len(batch.Spans)) + } + _, err := a.connUDP.Write(a.thriftBuffer.Bytes()) + return err +} + +// Close implements Close() of io.Closer and closes the underlying UDP connection. +func (a *agentClientUDP) Close() error { + return a.connUDP.Close() +} diff --git a/vendor/go.opencensus.io/exporter/jaeger/example/main.go b/vendor/go.opencensus.io/exporter/jaeger/example/main.go new file mode 100644 index 0000000000..303bc6ea15 --- /dev/null +++ b/vendor/go.opencensus.io/exporter/jaeger/example/main.go @@ -0,0 +1,60 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Command jaeger is an example program that creates spans +// and uploads to Jaeger. +package main + +import ( + "context" + "log" + + "go.opencensus.io/exporter/jaeger" + "go.opencensus.io/trace" +) + +func main() { + ctx := context.Background() + + // Register the Jaeger exporter to be able to retrieve + // the collected spans. + exporter, err := jaeger.NewExporter(jaeger.Options{ + Endpoint: "http://localhost:14268", + Process: jaeger.Process{ + ServiceName: "trace-demo", + }, + }) + if err != nil { + log.Fatal(err) + } + trace.RegisterExporter(exporter) + + // For demoing purposes, always sample. In a production application, you should + // configure this to a trace.ProbabilitySampler set at the desired + // probability. + trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) + + ctx, span := trace.StartSpan(ctx, "/foo") + bar(ctx) + span.End() + + exporter.Flush() +} + +func bar(ctx context.Context) { + ctx, span := trace.StartSpan(ctx, "/bar") + defer span.End() + + // Do bar... +} diff --git a/vendor/go.opencensus.io/exporter/jaeger/example_test.go b/vendor/go.opencensus.io/exporter/jaeger/example_test.go new file mode 100644 index 0000000000..bb21207e41 --- /dev/null +++ b/vendor/go.opencensus.io/exporter/jaeger/example_test.go @@ -0,0 +1,74 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jaeger_test + +import ( + "log" + + "go.opencensus.io/exporter/jaeger" + "go.opencensus.io/trace" +) + +func ExampleNewExporter_collector() { + // Register the Jaeger exporter to be able to retrieve + // the collected spans. + exporter, err := jaeger.NewExporter(jaeger.Options{ + Endpoint: "http://localhost:14268", + Process: jaeger.Process{ + ServiceName: "trace-demo", + }, + }) + if err != nil { + log.Fatal(err) + } + trace.RegisterExporter(exporter) +} + +func ExampleNewExporter_agent() { + // Register the Jaeger exporter to be able to retrieve + // the collected spans. + exporter, err := jaeger.NewExporter(jaeger.Options{ + AgentEndpoint: "localhost:6831", + Process: jaeger.Process{ + ServiceName: "trace-demo", + }, + }) + if err != nil { + log.Fatal(err) + } + trace.RegisterExporter(exporter) +} + +// ExampleNewExporter_processTags shows how to set ProcessTags +// on a Jaeger exporter. These tags will be added to the exported +// Jaeger process. +func ExampleNewExporter_processTags() { + // Register the Jaeger exporter to be able to retrieve + // the collected spans. + exporter, err := jaeger.NewExporter(jaeger.Options{ + AgentEndpoint: "localhost:6831", + Process: jaeger.Process{ + ServiceName: "trace-demo", + Tags: []jaeger.Tag{ + jaeger.StringTag("ip", "127.0.0.1"), + jaeger.BoolTag("demo", true), + }, + }, + }) + if err != nil { + log.Fatal(err) + } + trace.RegisterExporter(exporter) +} diff --git a/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/README b/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/README new file mode 100644 index 0000000000..cda0c56ffe --- /dev/null +++ b/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/README @@ -0,0 +1,2 @@ +Files autogenerated by the Thrift compiler +from the files at https://github.com/jaegertracing/jaeger-idl. \ No newline at end of file diff --git a/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/GoUnusedProtection__.go b/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/GoUnusedProtection__.go new file mode 100644 index 0000000000..345a65acb0 --- /dev/null +++ b/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/GoUnusedProtection__.go @@ -0,0 +1,6 @@ +// Autogenerated by Thrift Compiler (0.11.0) +// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING + +package jaeger + +var GoUnusedProtection__ int diff --git a/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/agent.go b/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/agent.go new file mode 100644 index 0000000000..e89bf49947 --- /dev/null +++ b/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/agent.go @@ -0,0 +1,244 @@ +// Autogenerated by Thrift Compiler (0.9.3) +// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING + +package jaeger + +import ( + "bytes" + "context" + "fmt" + + "git.apache.org/thrift.git/lib/go/thrift" +) + +// (needed to ensure safety because of naive import list construction.) +var _ = thrift.ZERO +var _ = fmt.Printf +var _ = bytes.Equal + +type Agent interface { + // Parameters: + // - Batch + EmitBatch(batch *Batch) (err error) +} + +type AgentClient struct { + Transport thrift.TTransport + ProtocolFactory thrift.TProtocolFactory + InputProtocol thrift.TProtocol + OutputProtocol thrift.TProtocol + SeqId int32 +} + +func NewAgentClientFactory(t thrift.TTransport, f thrift.TProtocolFactory) *AgentClient { + return &AgentClient{Transport: t, + ProtocolFactory: f, + InputProtocol: f.GetProtocol(t), + OutputProtocol: f.GetProtocol(t), + SeqId: 0, + } +} + +func NewAgentClientProtocol(t thrift.TTransport, iprot thrift.TProtocol, oprot thrift.TProtocol) *AgentClient { + return &AgentClient{Transport: t, + ProtocolFactory: nil, + InputProtocol: iprot, + OutputProtocol: oprot, + SeqId: 0, + } +} + +// Parameters: +// - Batch +func (p *AgentClient) EmitBatch(batch *Batch) (err error) { + if err = p.sendEmitBatch(batch); err != nil { + return + } + return +} + +func (p *AgentClient) sendEmitBatch(batch *Batch) (err error) { + oprot := p.OutputProtocol + if oprot == nil { + oprot = p.ProtocolFactory.GetProtocol(p.Transport) + p.OutputProtocol = oprot + } + p.SeqId++ + if err = oprot.WriteMessageBegin("emitBatch", thrift.ONEWAY, p.SeqId); err != nil { + return + } + args := AgentEmitBatchArgs{ + Batch: batch, + } + if err = args.Write(oprot); err != nil { + return + } + if err = oprot.WriteMessageEnd(); err != nil { + return + } + return oprot.Flush(context.Background()) +} + +type AgentProcessor struct { + processorMap map[string]thrift.TProcessorFunction + handler Agent +} + +func (p *AgentProcessor) AddToProcessorMap(key string, processor thrift.TProcessorFunction) { + p.processorMap[key] = processor +} + +func (p *AgentProcessor) GetProcessorFunction(key string) (processor thrift.TProcessorFunction, ok bool) { + processor, ok = p.processorMap[key] + return processor, ok +} + +func (p *AgentProcessor) ProcessorMap() map[string]thrift.TProcessorFunction { + return p.processorMap +} + +func NewAgentProcessor(handler Agent) *AgentProcessor { + + self0 := &AgentProcessor{handler: handler, processorMap: make(map[string]thrift.TProcessorFunction)} + self0.processorMap["emitBatch"] = &agentProcessorEmitBatch{handler: handler} + return self0 +} + +func (p *AgentProcessor) Process(iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + ctx := context.Background() + name, _, seqId, err := iprot.ReadMessageBegin() + if err != nil { + return false, err + } + if processor, ok := p.GetProcessorFunction(name); ok { + return processor.Process(ctx, seqId, iprot, oprot) + } + iprot.Skip(thrift.STRUCT) + iprot.ReadMessageEnd() + x1 := thrift.NewTApplicationException(thrift.UNKNOWN_METHOD, "Unknown function "+name) + oprot.WriteMessageBegin(name, thrift.EXCEPTION, seqId) + x1.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return false, x1 +} + +type agentProcessorEmitBatch struct { + handler Agent +} + +func (p *agentProcessorEmitBatch) Process(ctx context.Context, seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + args := AgentEmitBatchArgs{} + if err = args.Read(iprot); err != nil { + iprot.ReadMessageEnd() + return false, err + } + + iprot.ReadMessageEnd() + var err2 error + if err2 = p.handler.EmitBatch(args.Batch); err2 != nil { + return true, err2 + } + return true, nil +} + +// HELPER FUNCTIONS AND STRUCTURES + +// Attributes: +// - Batch +type AgentEmitBatchArgs struct { + Batch *Batch `thrift:"batch,1" json:"batch"` +} + +func NewAgentEmitBatchArgs() *AgentEmitBatchArgs { + return &AgentEmitBatchArgs{} +} + +var AgentEmitBatchArgs_Batch_DEFAULT *Batch + +func (p *AgentEmitBatchArgs) GetBatch() *Batch { + if !p.IsSetBatch() { + return AgentEmitBatchArgs_Batch_DEFAULT + } + return p.Batch +} +func (p *AgentEmitBatchArgs) IsSetBatch() bool { + return p.Batch != nil +} + +func (p *AgentEmitBatchArgs) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) + } + + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if err := p.readField1(iprot); err != nil { + return err + } + default: + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + if err := iprot.ReadFieldEnd(); err != nil { + return err + } + } + if err := iprot.ReadStructEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) + } + return nil +} + +func (p *AgentEmitBatchArgs) readField1(iprot thrift.TProtocol) error { + p.Batch = &Batch{} + if err := p.Batch.Read(iprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", p.Batch), err) + } + return nil +} + +func (p *AgentEmitBatchArgs) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("emitBatch_args"); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) + } + if err := p.writeField1(oprot); err != nil { + return err + } + if err := oprot.WriteFieldStop(); err != nil { + return thrift.PrependError("write field stop error: ", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return thrift.PrependError("write struct stop error: ", err) + } + return nil +} + +func (p *AgentEmitBatchArgs) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("batch", thrift.STRUCT, 1); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:batch: ", p), err) + } + if err := p.Batch.Write(oprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", p.Batch), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 1:batch: ", p), err) + } + return err +} + +func (p *AgentEmitBatchArgs) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("AgentEmitBatchArgs(%+v)", *p) +} diff --git a/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/collector-remote/collector-remote.go b/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/collector-remote/collector-remote.go new file mode 100755 index 0000000000..e367bc243a --- /dev/null +++ b/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/collector-remote/collector-remote.go @@ -0,0 +1,155 @@ +// Autogenerated by Thrift Compiler (0.11.0) +// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING + +package main + +import ( + "context" + "flag" + "fmt" + "math" + "net" + "net/url" + "os" + "strconv" + "strings" + + "git.apache.org/thrift.git/lib/go/thrift" + "go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger" +) + +func Usage() { + fmt.Fprintln(os.Stderr, "Usage of ", os.Args[0], " [-h host:port] [-u url] [-f[ramed]] function [arg1 [arg2...]]:") + flag.PrintDefaults() + fmt.Fprintln(os.Stderr, "\nFunctions:") + fmt.Fprintln(os.Stderr, " submitBatches( batches)") + fmt.Fprintln(os.Stderr) + os.Exit(0) +} + +func main() { + flag.Usage = Usage + var host string + var port int + var protocol string + var urlString string + var framed bool + var useHttp bool + var parsedUrl *url.URL + var trans thrift.TTransport + _ = strconv.Atoi + _ = math.Abs + flag.Usage = Usage + flag.StringVar(&host, "h", "localhost", "Specify host and port") + flag.IntVar(&port, "p", 9090, "Specify port") + flag.StringVar(&protocol, "P", "binary", "Specify the protocol (binary, compact, simplejson, json)") + flag.StringVar(&urlString, "u", "", "Specify the url") + flag.BoolVar(&framed, "framed", false, "Use framed transport") + flag.BoolVar(&useHttp, "http", false, "Use http") + flag.Parse() + + if len(urlString) > 0 { + var err error + parsedUrl, err = url.Parse(urlString) + if err != nil { + fmt.Fprintln(os.Stderr, "Error parsing URL: ", err) + flag.Usage() + } + host = parsedUrl.Host + useHttp = len(parsedUrl.Scheme) <= 0 || parsedUrl.Scheme == "http" + } else if useHttp { + _, err := url.Parse(fmt.Sprint("http://", host, ":", port)) + if err != nil { + fmt.Fprintln(os.Stderr, "Error parsing URL: ", err) + flag.Usage() + } + } + + cmd := flag.Arg(0) + var err error + if useHttp { + trans, err = thrift.NewTHttpClient(parsedUrl.String()) + } else { + portStr := fmt.Sprint(port) + if strings.Contains(host, ":") { + host, portStr, err = net.SplitHostPort(host) + if err != nil { + fmt.Fprintln(os.Stderr, "error with host:", err) + os.Exit(1) + } + } + trans, err = thrift.NewTSocket(net.JoinHostPort(host, portStr)) + if err != nil { + fmt.Fprintln(os.Stderr, "error resolving address:", err) + os.Exit(1) + } + if framed { + trans = thrift.NewTFramedTransport(trans) + } + } + if err != nil { + fmt.Fprintln(os.Stderr, "Error creating transport", err) + os.Exit(1) + } + defer trans.Close() + var protocolFactory thrift.TProtocolFactory + switch protocol { + case "compact": + protocolFactory = thrift.NewTCompactProtocolFactory() + break + case "simplejson": + protocolFactory = thrift.NewTSimpleJSONProtocolFactory() + break + case "json": + protocolFactory = thrift.NewTJSONProtocolFactory() + break + case "binary", "": + protocolFactory = thrift.NewTBinaryProtocolFactoryDefault() + break + default: + fmt.Fprintln(os.Stderr, "Invalid protocol specified: ", protocol) + Usage() + os.Exit(1) + } + iprot := protocolFactory.GetProtocol(trans) + oprot := protocolFactory.GetProtocol(trans) + client := jaeger.NewCollectorClient(thrift.NewTStandardClient(iprot, oprot)) + if err := trans.Open(); err != nil { + fmt.Fprintln(os.Stderr, "Error opening socket to ", host, ":", port, " ", err) + os.Exit(1) + } + + switch cmd { + case "submitBatches": + if flag.NArg()-1 != 1 { + fmt.Fprintln(os.Stderr, "SubmitBatches requires 1 args") + flag.Usage() + } + arg12 := flag.Arg(1) + mbTrans13 := thrift.NewTMemoryBufferLen(len(arg12)) + defer mbTrans13.Close() + _, err14 := mbTrans13.WriteString(arg12) + if err14 != nil { + Usage() + return + } + factory15 := thrift.NewTSimpleJSONProtocolFactory() + jsProt16 := factory15.GetProtocol(mbTrans13) + containerStruct0 := jaeger.NewCollectorSubmitBatchesArgs() + err17 := containerStruct0.ReadField1(jsProt16) + if err17 != nil { + Usage() + return + } + argvalue0 := containerStruct0.Batches + value0 := argvalue0 + fmt.Print(client.SubmitBatches(context.Background(), value0)) + fmt.Print("\n") + break + case "": + Usage() + break + default: + fmt.Fprintln(os.Stderr, "Invalid function ", cmd) + } +} diff --git a/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/jaeger-consts.go b/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/jaeger-consts.go new file mode 100644 index 0000000000..80bced8dc2 --- /dev/null +++ b/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/jaeger-consts.go @@ -0,0 +1,23 @@ +// Autogenerated by Thrift Compiler (0.11.0) +// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING + +package jaeger + +import ( + "bytes" + "context" + "fmt" + "reflect" + + "git.apache.org/thrift.git/lib/go/thrift" +) + +// (needed to ensure safety because of naive import list construction.) +var _ = thrift.ZERO +var _ = fmt.Printf +var _ = context.Background +var _ = reflect.DeepEqual +var _ = bytes.Equal + +func init() { +} diff --git a/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/jaeger.go b/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/jaeger.go new file mode 100644 index 0000000000..8d5d796aec --- /dev/null +++ b/vendor/go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger/jaeger.go @@ -0,0 +1,2443 @@ +// Autogenerated by Thrift Compiler (0.11.0) +// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING + +package jaeger // import "go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger" + +import ( + "bytes" + "context" + "database/sql/driver" + "errors" + "fmt" + "reflect" + + "git.apache.org/thrift.git/lib/go/thrift" +) + +// (needed to ensure safety because of naive import list construction.) +var _ = thrift.ZERO +var _ = fmt.Printf +var _ = context.Background +var _ = reflect.DeepEqual +var _ = bytes.Equal + +type TagType int64 + +const ( + TagType_STRING TagType = 0 + TagType_DOUBLE TagType = 1 + TagType_BOOL TagType = 2 + TagType_LONG TagType = 3 + TagType_BINARY TagType = 4 +) + +func (p TagType) String() string { + switch p { + case TagType_STRING: + return "STRING" + case TagType_DOUBLE: + return "DOUBLE" + case TagType_BOOL: + return "BOOL" + case TagType_LONG: + return "LONG" + case TagType_BINARY: + return "BINARY" + } + return "" +} + +func TagTypeFromString(s string) (TagType, error) { + switch s { + case "STRING": + return TagType_STRING, nil + case "DOUBLE": + return TagType_DOUBLE, nil + case "BOOL": + return TagType_BOOL, nil + case "LONG": + return TagType_LONG, nil + case "BINARY": + return TagType_BINARY, nil + } + return TagType(0), fmt.Errorf("not a valid TagType string") +} + +func TagTypePtr(v TagType) *TagType { return &v } + +func (p TagType) MarshalText() ([]byte, error) { + return []byte(p.String()), nil +} + +func (p *TagType) UnmarshalText(text []byte) error { + q, err := TagTypeFromString(string(text)) + if err != nil { + return err + } + *p = q + return nil +} + +func (p *TagType) Scan(value interface{}) error { + v, ok := value.(int64) + if !ok { + return errors.New("Scan value is not int64") + } + *p = TagType(v) + return nil +} + +func (p *TagType) Value() (driver.Value, error) { + if p == nil { + return nil, nil + } + return int64(*p), nil +} + +type SpanRefType int64 + +const ( + SpanRefType_CHILD_OF SpanRefType = 0 + SpanRefType_FOLLOWS_FROM SpanRefType = 1 +) + +func (p SpanRefType) String() string { + switch p { + case SpanRefType_CHILD_OF: + return "CHILD_OF" + case SpanRefType_FOLLOWS_FROM: + return "FOLLOWS_FROM" + } + return "" +} + +func SpanRefTypeFromString(s string) (SpanRefType, error) { + switch s { + case "CHILD_OF": + return SpanRefType_CHILD_OF, nil + case "FOLLOWS_FROM": + return SpanRefType_FOLLOWS_FROM, nil + } + return SpanRefType(0), fmt.Errorf("not a valid SpanRefType string") +} + +func SpanRefTypePtr(v SpanRefType) *SpanRefType { return &v } + +func (p SpanRefType) MarshalText() ([]byte, error) { + return []byte(p.String()), nil +} + +func (p *SpanRefType) UnmarshalText(text []byte) error { + q, err := SpanRefTypeFromString(string(text)) + if err != nil { + return err + } + *p = q + return nil +} + +func (p *SpanRefType) Scan(value interface{}) error { + v, ok := value.(int64) + if !ok { + return errors.New("Scan value is not int64") + } + *p = SpanRefType(v) + return nil +} + +func (p *SpanRefType) Value() (driver.Value, error) { + if p == nil { + return nil, nil + } + return int64(*p), nil +} + +// Attributes: +// - Key +// - VType +// - VStr +// - VDouble +// - VBool +// - VLong +// - VBinary +type Tag struct { + Key string `thrift:"key,1,required" db:"key" json:"key"` + VType TagType `thrift:"vType,2,required" db:"vType" json:"vType"` + VStr *string `thrift:"vStr,3" db:"vStr" json:"vStr,omitempty"` + VDouble *float64 `thrift:"vDouble,4" db:"vDouble" json:"vDouble,omitempty"` + VBool *bool `thrift:"vBool,5" db:"vBool" json:"vBool,omitempty"` + VLong *int64 `thrift:"vLong,6" db:"vLong" json:"vLong,omitempty"` + VBinary []byte `thrift:"vBinary,7" db:"vBinary" json:"vBinary,omitempty"` +} + +func NewTag() *Tag { + return &Tag{} +} + +func (p *Tag) GetKey() string { + return p.Key +} + +func (p *Tag) GetVType() TagType { + return p.VType +} + +var Tag_VStr_DEFAULT string + +func (p *Tag) GetVStr() string { + if !p.IsSetVStr() { + return Tag_VStr_DEFAULT + } + return *p.VStr +} + +var Tag_VDouble_DEFAULT float64 + +func (p *Tag) GetVDouble() float64 { + if !p.IsSetVDouble() { + return Tag_VDouble_DEFAULT + } + return *p.VDouble +} + +var Tag_VBool_DEFAULT bool + +func (p *Tag) GetVBool() bool { + if !p.IsSetVBool() { + return Tag_VBool_DEFAULT + } + return *p.VBool +} + +var Tag_VLong_DEFAULT int64 + +func (p *Tag) GetVLong() int64 { + if !p.IsSetVLong() { + return Tag_VLong_DEFAULT + } + return *p.VLong +} + +var Tag_VBinary_DEFAULT []byte + +func (p *Tag) GetVBinary() []byte { + return p.VBinary +} +func (p *Tag) IsSetVStr() bool { + return p.VStr != nil +} + +func (p *Tag) IsSetVDouble() bool { + return p.VDouble != nil +} + +func (p *Tag) IsSetVBool() bool { + return p.VBool != nil +} + +func (p *Tag) IsSetVLong() bool { + return p.VLong != nil +} + +func (p *Tag) IsSetVBinary() bool { + return p.VBinary != nil +} + +func (p *Tag) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) + } + + var issetKey bool = false + var issetVType bool = false + + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if fieldTypeId == thrift.STRING { + if err := p.ReadField1(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetKey = true + case 2: + if fieldTypeId == thrift.I32 { + if err := p.ReadField2(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetVType = true + case 3: + if fieldTypeId == thrift.STRING { + if err := p.ReadField3(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + case 4: + if fieldTypeId == thrift.DOUBLE { + if err := p.ReadField4(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + case 5: + if fieldTypeId == thrift.BOOL { + if err := p.ReadField5(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + case 6: + if fieldTypeId == thrift.I64 { + if err := p.ReadField6(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + case 7: + if fieldTypeId == thrift.STRING { + if err := p.ReadField7(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + default: + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + if err := iprot.ReadFieldEnd(); err != nil { + return err + } + } + if err := iprot.ReadStructEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) + } + if !issetKey { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field Key is not set")) + } + if !issetVType { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field VType is not set")) + } + return nil +} + +func (p *Tag) ReadField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadString(); err != nil { + return thrift.PrependError("error reading field 1: ", err) + } else { + p.Key = v + } + return nil +} + +func (p *Tag) ReadField2(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI32(); err != nil { + return thrift.PrependError("error reading field 2: ", err) + } else { + temp := TagType(v) + p.VType = temp + } + return nil +} + +func (p *Tag) ReadField3(iprot thrift.TProtocol) error { + if v, err := iprot.ReadString(); err != nil { + return thrift.PrependError("error reading field 3: ", err) + } else { + p.VStr = &v + } + return nil +} + +func (p *Tag) ReadField4(iprot thrift.TProtocol) error { + if v, err := iprot.ReadDouble(); err != nil { + return thrift.PrependError("error reading field 4: ", err) + } else { + p.VDouble = &v + } + return nil +} + +func (p *Tag) ReadField5(iprot thrift.TProtocol) error { + if v, err := iprot.ReadBool(); err != nil { + return thrift.PrependError("error reading field 5: ", err) + } else { + p.VBool = &v + } + return nil +} + +func (p *Tag) ReadField6(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI64(); err != nil { + return thrift.PrependError("error reading field 6: ", err) + } else { + p.VLong = &v + } + return nil +} + +func (p *Tag) ReadField7(iprot thrift.TProtocol) error { + if v, err := iprot.ReadBinary(); err != nil { + return thrift.PrependError("error reading field 7: ", err) + } else { + p.VBinary = v + } + return nil +} + +func (p *Tag) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("Tag"); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) + } + if p != nil { + if err := p.writeField1(oprot); err != nil { + return err + } + if err := p.writeField2(oprot); err != nil { + return err + } + if err := p.writeField3(oprot); err != nil { + return err + } + if err := p.writeField4(oprot); err != nil { + return err + } + if err := p.writeField5(oprot); err != nil { + return err + } + if err := p.writeField6(oprot); err != nil { + return err + } + if err := p.writeField7(oprot); err != nil { + return err + } + } + if err := oprot.WriteFieldStop(); err != nil { + return thrift.PrependError("write field stop error: ", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return thrift.PrependError("write struct stop error: ", err) + } + return nil +} + +func (p *Tag) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("key", thrift.STRING, 1); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:key: ", p), err) + } + if err := oprot.WriteString(string(p.Key)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.key (1) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 1:key: ", p), err) + } + return err +} + +func (p *Tag) writeField2(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("vType", thrift.I32, 2); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:vType: ", p), err) + } + if err := oprot.WriteI32(int32(p.VType)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.vType (2) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 2:vType: ", p), err) + } + return err +} + +func (p *Tag) writeField3(oprot thrift.TProtocol) (err error) { + if p.IsSetVStr() { + if err := oprot.WriteFieldBegin("vStr", thrift.STRING, 3); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 3:vStr: ", p), err) + } + if err := oprot.WriteString(string(*p.VStr)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.vStr (3) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 3:vStr: ", p), err) + } + } + return err +} + +func (p *Tag) writeField4(oprot thrift.TProtocol) (err error) { + if p.IsSetVDouble() { + if err := oprot.WriteFieldBegin("vDouble", thrift.DOUBLE, 4); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 4:vDouble: ", p), err) + } + if err := oprot.WriteDouble(float64(*p.VDouble)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.vDouble (4) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 4:vDouble: ", p), err) + } + } + return err +} + +func (p *Tag) writeField5(oprot thrift.TProtocol) (err error) { + if p.IsSetVBool() { + if err := oprot.WriteFieldBegin("vBool", thrift.BOOL, 5); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 5:vBool: ", p), err) + } + if err := oprot.WriteBool(bool(*p.VBool)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.vBool (5) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 5:vBool: ", p), err) + } + } + return err +} + +func (p *Tag) writeField6(oprot thrift.TProtocol) (err error) { + if p.IsSetVLong() { + if err := oprot.WriteFieldBegin("vLong", thrift.I64, 6); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 6:vLong: ", p), err) + } + if err := oprot.WriteI64(int64(*p.VLong)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.vLong (6) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 6:vLong: ", p), err) + } + } + return err +} + +func (p *Tag) writeField7(oprot thrift.TProtocol) (err error) { + if p.IsSetVBinary() { + if err := oprot.WriteFieldBegin("vBinary", thrift.STRING, 7); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 7:vBinary: ", p), err) + } + if err := oprot.WriteBinary(p.VBinary); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.vBinary (7) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 7:vBinary: ", p), err) + } + } + return err +} + +func (p *Tag) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("Tag(%+v)", *p) +} + +// Attributes: +// - Timestamp +// - Fields +type Log struct { + Timestamp int64 `thrift:"timestamp,1,required" db:"timestamp" json:"timestamp"` + Fields []*Tag `thrift:"fields,2,required" db:"fields" json:"fields"` +} + +func NewLog() *Log { + return &Log{} +} + +func (p *Log) GetTimestamp() int64 { + return p.Timestamp +} + +func (p *Log) GetFields() []*Tag { + return p.Fields +} +func (p *Log) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) + } + + var issetTimestamp bool = false + var issetFields bool = false + + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if fieldTypeId == thrift.I64 { + if err := p.ReadField1(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetTimestamp = true + case 2: + if fieldTypeId == thrift.LIST { + if err := p.ReadField2(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetFields = true + default: + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + if err := iprot.ReadFieldEnd(); err != nil { + return err + } + } + if err := iprot.ReadStructEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) + } + if !issetTimestamp { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field Timestamp is not set")) + } + if !issetFields { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field Fields is not set")) + } + return nil +} + +func (p *Log) ReadField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI64(); err != nil { + return thrift.PrependError("error reading field 1: ", err) + } else { + p.Timestamp = v + } + return nil +} + +func (p *Log) ReadField2(iprot thrift.TProtocol) error { + _, size, err := iprot.ReadListBegin() + if err != nil { + return thrift.PrependError("error reading list begin: ", err) + } + tSlice := make([]*Tag, 0, size) + p.Fields = tSlice + for i := 0; i < size; i++ { + _elem0 := &Tag{} + if err := _elem0.Read(iprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", _elem0), err) + } + p.Fields = append(p.Fields, _elem0) + } + if err := iprot.ReadListEnd(); err != nil { + return thrift.PrependError("error reading list end: ", err) + } + return nil +} + +func (p *Log) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("Log"); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) + } + if p != nil { + if err := p.writeField1(oprot); err != nil { + return err + } + if err := p.writeField2(oprot); err != nil { + return err + } + } + if err := oprot.WriteFieldStop(); err != nil { + return thrift.PrependError("write field stop error: ", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return thrift.PrependError("write struct stop error: ", err) + } + return nil +} + +func (p *Log) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("timestamp", thrift.I64, 1); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:timestamp: ", p), err) + } + if err := oprot.WriteI64(int64(p.Timestamp)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.timestamp (1) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 1:timestamp: ", p), err) + } + return err +} + +func (p *Log) writeField2(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("fields", thrift.LIST, 2); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:fields: ", p), err) + } + if err := oprot.WriteListBegin(thrift.STRUCT, len(p.Fields)); err != nil { + return thrift.PrependError("error writing list begin: ", err) + } + for _, v := range p.Fields { + if err := v.Write(oprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", v), err) + } + } + if err := oprot.WriteListEnd(); err != nil { + return thrift.PrependError("error writing list end: ", err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 2:fields: ", p), err) + } + return err +} + +func (p *Log) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("Log(%+v)", *p) +} + +// Attributes: +// - RefType +// - TraceIdLow +// - TraceIdHigh +// - SpanId +type SpanRef struct { + RefType SpanRefType `thrift:"refType,1,required" db:"refType" json:"refType"` + TraceIdLow int64 `thrift:"traceIdLow,2,required" db:"traceIdLow" json:"traceIdLow"` + TraceIdHigh int64 `thrift:"traceIdHigh,3,required" db:"traceIdHigh" json:"traceIdHigh"` + SpanId int64 `thrift:"spanId,4,required" db:"spanId" json:"spanId"` +} + +func NewSpanRef() *SpanRef { + return &SpanRef{} +} + +func (p *SpanRef) GetRefType() SpanRefType { + return p.RefType +} + +func (p *SpanRef) GetTraceIdLow() int64 { + return p.TraceIdLow +} + +func (p *SpanRef) GetTraceIdHigh() int64 { + return p.TraceIdHigh +} + +func (p *SpanRef) GetSpanId() int64 { + return p.SpanId +} +func (p *SpanRef) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) + } + + var issetRefType bool = false + var issetTraceIdLow bool = false + var issetTraceIdHigh bool = false + var issetSpanId bool = false + + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if fieldTypeId == thrift.I32 { + if err := p.ReadField1(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetRefType = true + case 2: + if fieldTypeId == thrift.I64 { + if err := p.ReadField2(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetTraceIdLow = true + case 3: + if fieldTypeId == thrift.I64 { + if err := p.ReadField3(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetTraceIdHigh = true + case 4: + if fieldTypeId == thrift.I64 { + if err := p.ReadField4(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetSpanId = true + default: + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + if err := iprot.ReadFieldEnd(); err != nil { + return err + } + } + if err := iprot.ReadStructEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) + } + if !issetRefType { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field RefType is not set")) + } + if !issetTraceIdLow { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field TraceIdLow is not set")) + } + if !issetTraceIdHigh { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field TraceIdHigh is not set")) + } + if !issetSpanId { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field SpanId is not set")) + } + return nil +} + +func (p *SpanRef) ReadField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI32(); err != nil { + return thrift.PrependError("error reading field 1: ", err) + } else { + temp := SpanRefType(v) + p.RefType = temp + } + return nil +} + +func (p *SpanRef) ReadField2(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI64(); err != nil { + return thrift.PrependError("error reading field 2: ", err) + } else { + p.TraceIdLow = v + } + return nil +} + +func (p *SpanRef) ReadField3(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI64(); err != nil { + return thrift.PrependError("error reading field 3: ", err) + } else { + p.TraceIdHigh = v + } + return nil +} + +func (p *SpanRef) ReadField4(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI64(); err != nil { + return thrift.PrependError("error reading field 4: ", err) + } else { + p.SpanId = v + } + return nil +} + +func (p *SpanRef) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("SpanRef"); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) + } + if p != nil { + if err := p.writeField1(oprot); err != nil { + return err + } + if err := p.writeField2(oprot); err != nil { + return err + } + if err := p.writeField3(oprot); err != nil { + return err + } + if err := p.writeField4(oprot); err != nil { + return err + } + } + if err := oprot.WriteFieldStop(); err != nil { + return thrift.PrependError("write field stop error: ", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return thrift.PrependError("write struct stop error: ", err) + } + return nil +} + +func (p *SpanRef) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("refType", thrift.I32, 1); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:refType: ", p), err) + } + if err := oprot.WriteI32(int32(p.RefType)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.refType (1) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 1:refType: ", p), err) + } + return err +} + +func (p *SpanRef) writeField2(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("traceIdLow", thrift.I64, 2); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:traceIdLow: ", p), err) + } + if err := oprot.WriteI64(int64(p.TraceIdLow)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.traceIdLow (2) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 2:traceIdLow: ", p), err) + } + return err +} + +func (p *SpanRef) writeField3(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("traceIdHigh", thrift.I64, 3); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 3:traceIdHigh: ", p), err) + } + if err := oprot.WriteI64(int64(p.TraceIdHigh)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.traceIdHigh (3) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 3:traceIdHigh: ", p), err) + } + return err +} + +func (p *SpanRef) writeField4(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("spanId", thrift.I64, 4); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 4:spanId: ", p), err) + } + if err := oprot.WriteI64(int64(p.SpanId)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.spanId (4) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 4:spanId: ", p), err) + } + return err +} + +func (p *SpanRef) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("SpanRef(%+v)", *p) +} + +// Attributes: +// - TraceIdLow +// - TraceIdHigh +// - SpanId +// - ParentSpanId +// - OperationName +// - References +// - Flags +// - StartTime +// - Duration +// - Tags +// - Logs +type Span struct { + TraceIdLow int64 `thrift:"traceIdLow,1,required" db:"traceIdLow" json:"traceIdLow"` + TraceIdHigh int64 `thrift:"traceIdHigh,2,required" db:"traceIdHigh" json:"traceIdHigh"` + SpanId int64 `thrift:"spanId,3,required" db:"spanId" json:"spanId"` + ParentSpanId int64 `thrift:"parentSpanId,4,required" db:"parentSpanId" json:"parentSpanId"` + OperationName string `thrift:"operationName,5,required" db:"operationName" json:"operationName"` + References []*SpanRef `thrift:"references,6" db:"references" json:"references,omitempty"` + Flags int32 `thrift:"flags,7,required" db:"flags" json:"flags"` + StartTime int64 `thrift:"startTime,8,required" db:"startTime" json:"startTime"` + Duration int64 `thrift:"duration,9,required" db:"duration" json:"duration"` + Tags []*Tag `thrift:"tags,10" db:"tags" json:"tags,omitempty"` + Logs []*Log `thrift:"logs,11" db:"logs" json:"logs,omitempty"` +} + +func NewSpan() *Span { + return &Span{} +} + +func (p *Span) GetTraceIdLow() int64 { + return p.TraceIdLow +} + +func (p *Span) GetTraceIdHigh() int64 { + return p.TraceIdHigh +} + +func (p *Span) GetSpanId() int64 { + return p.SpanId +} + +func (p *Span) GetParentSpanId() int64 { + return p.ParentSpanId +} + +func (p *Span) GetOperationName() string { + return p.OperationName +} + +var Span_References_DEFAULT []*SpanRef + +func (p *Span) GetReferences() []*SpanRef { + return p.References +} + +func (p *Span) GetFlags() int32 { + return p.Flags +} + +func (p *Span) GetStartTime() int64 { + return p.StartTime +} + +func (p *Span) GetDuration() int64 { + return p.Duration +} + +var Span_Tags_DEFAULT []*Tag + +func (p *Span) GetTags() []*Tag { + return p.Tags +} + +var Span_Logs_DEFAULT []*Log + +func (p *Span) GetLogs() []*Log { + return p.Logs +} +func (p *Span) IsSetReferences() bool { + return p.References != nil +} + +func (p *Span) IsSetTags() bool { + return p.Tags != nil +} + +func (p *Span) IsSetLogs() bool { + return p.Logs != nil +} + +func (p *Span) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) + } + + var issetTraceIdLow bool = false + var issetTraceIdHigh bool = false + var issetSpanId bool = false + var issetParentSpanId bool = false + var issetOperationName bool = false + var issetFlags bool = false + var issetStartTime bool = false + var issetDuration bool = false + + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if fieldTypeId == thrift.I64 { + if err := p.ReadField1(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetTraceIdLow = true + case 2: + if fieldTypeId == thrift.I64 { + if err := p.ReadField2(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetTraceIdHigh = true + case 3: + if fieldTypeId == thrift.I64 { + if err := p.ReadField3(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetSpanId = true + case 4: + if fieldTypeId == thrift.I64 { + if err := p.ReadField4(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetParentSpanId = true + case 5: + if fieldTypeId == thrift.STRING { + if err := p.ReadField5(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetOperationName = true + case 6: + if fieldTypeId == thrift.LIST { + if err := p.ReadField6(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + case 7: + if fieldTypeId == thrift.I32 { + if err := p.ReadField7(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetFlags = true + case 8: + if fieldTypeId == thrift.I64 { + if err := p.ReadField8(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetStartTime = true + case 9: + if fieldTypeId == thrift.I64 { + if err := p.ReadField9(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetDuration = true + case 10: + if fieldTypeId == thrift.LIST { + if err := p.ReadField10(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + case 11: + if fieldTypeId == thrift.LIST { + if err := p.ReadField11(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + default: + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + if err := iprot.ReadFieldEnd(); err != nil { + return err + } + } + if err := iprot.ReadStructEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) + } + if !issetTraceIdLow { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field TraceIdLow is not set")) + } + if !issetTraceIdHigh { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field TraceIdHigh is not set")) + } + if !issetSpanId { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field SpanId is not set")) + } + if !issetParentSpanId { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field ParentSpanId is not set")) + } + if !issetOperationName { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field OperationName is not set")) + } + if !issetFlags { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field Flags is not set")) + } + if !issetStartTime { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field StartTime is not set")) + } + if !issetDuration { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field Duration is not set")) + } + return nil +} + +func (p *Span) ReadField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI64(); err != nil { + return thrift.PrependError("error reading field 1: ", err) + } else { + p.TraceIdLow = v + } + return nil +} + +func (p *Span) ReadField2(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI64(); err != nil { + return thrift.PrependError("error reading field 2: ", err) + } else { + p.TraceIdHigh = v + } + return nil +} + +func (p *Span) ReadField3(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI64(); err != nil { + return thrift.PrependError("error reading field 3: ", err) + } else { + p.SpanId = v + } + return nil +} + +func (p *Span) ReadField4(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI64(); err != nil { + return thrift.PrependError("error reading field 4: ", err) + } else { + p.ParentSpanId = v + } + return nil +} + +func (p *Span) ReadField5(iprot thrift.TProtocol) error { + if v, err := iprot.ReadString(); err != nil { + return thrift.PrependError("error reading field 5: ", err) + } else { + p.OperationName = v + } + return nil +} + +func (p *Span) ReadField6(iprot thrift.TProtocol) error { + _, size, err := iprot.ReadListBegin() + if err != nil { + return thrift.PrependError("error reading list begin: ", err) + } + tSlice := make([]*SpanRef, 0, size) + p.References = tSlice + for i := 0; i < size; i++ { + _elem1 := &SpanRef{} + if err := _elem1.Read(iprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", _elem1), err) + } + p.References = append(p.References, _elem1) + } + if err := iprot.ReadListEnd(); err != nil { + return thrift.PrependError("error reading list end: ", err) + } + return nil +} + +func (p *Span) ReadField7(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI32(); err != nil { + return thrift.PrependError("error reading field 7: ", err) + } else { + p.Flags = v + } + return nil +} + +func (p *Span) ReadField8(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI64(); err != nil { + return thrift.PrependError("error reading field 8: ", err) + } else { + p.StartTime = v + } + return nil +} + +func (p *Span) ReadField9(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI64(); err != nil { + return thrift.PrependError("error reading field 9: ", err) + } else { + p.Duration = v + } + return nil +} + +func (p *Span) ReadField10(iprot thrift.TProtocol) error { + _, size, err := iprot.ReadListBegin() + if err != nil { + return thrift.PrependError("error reading list begin: ", err) + } + tSlice := make([]*Tag, 0, size) + p.Tags = tSlice + for i := 0; i < size; i++ { + _elem2 := &Tag{} + if err := _elem2.Read(iprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", _elem2), err) + } + p.Tags = append(p.Tags, _elem2) + } + if err := iprot.ReadListEnd(); err != nil { + return thrift.PrependError("error reading list end: ", err) + } + return nil +} + +func (p *Span) ReadField11(iprot thrift.TProtocol) error { + _, size, err := iprot.ReadListBegin() + if err != nil { + return thrift.PrependError("error reading list begin: ", err) + } + tSlice := make([]*Log, 0, size) + p.Logs = tSlice + for i := 0; i < size; i++ { + _elem3 := &Log{} + if err := _elem3.Read(iprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", _elem3), err) + } + p.Logs = append(p.Logs, _elem3) + } + if err := iprot.ReadListEnd(); err != nil { + return thrift.PrependError("error reading list end: ", err) + } + return nil +} + +func (p *Span) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("Span"); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) + } + if p != nil { + if err := p.writeField1(oprot); err != nil { + return err + } + if err := p.writeField2(oprot); err != nil { + return err + } + if err := p.writeField3(oprot); err != nil { + return err + } + if err := p.writeField4(oprot); err != nil { + return err + } + if err := p.writeField5(oprot); err != nil { + return err + } + if err := p.writeField6(oprot); err != nil { + return err + } + if err := p.writeField7(oprot); err != nil { + return err + } + if err := p.writeField8(oprot); err != nil { + return err + } + if err := p.writeField9(oprot); err != nil { + return err + } + if err := p.writeField10(oprot); err != nil { + return err + } + if err := p.writeField11(oprot); err != nil { + return err + } + } + if err := oprot.WriteFieldStop(); err != nil { + return thrift.PrependError("write field stop error: ", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return thrift.PrependError("write struct stop error: ", err) + } + return nil +} + +func (p *Span) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("traceIdLow", thrift.I64, 1); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:traceIdLow: ", p), err) + } + if err := oprot.WriteI64(int64(p.TraceIdLow)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.traceIdLow (1) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 1:traceIdLow: ", p), err) + } + return err +} + +func (p *Span) writeField2(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("traceIdHigh", thrift.I64, 2); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:traceIdHigh: ", p), err) + } + if err := oprot.WriteI64(int64(p.TraceIdHigh)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.traceIdHigh (2) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 2:traceIdHigh: ", p), err) + } + return err +} + +func (p *Span) writeField3(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("spanId", thrift.I64, 3); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 3:spanId: ", p), err) + } + if err := oprot.WriteI64(int64(p.SpanId)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.spanId (3) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 3:spanId: ", p), err) + } + return err +} + +func (p *Span) writeField4(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("parentSpanId", thrift.I64, 4); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 4:parentSpanId: ", p), err) + } + if err := oprot.WriteI64(int64(p.ParentSpanId)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.parentSpanId (4) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 4:parentSpanId: ", p), err) + } + return err +} + +func (p *Span) writeField5(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("operationName", thrift.STRING, 5); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 5:operationName: ", p), err) + } + if err := oprot.WriteString(string(p.OperationName)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.operationName (5) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 5:operationName: ", p), err) + } + return err +} + +func (p *Span) writeField6(oprot thrift.TProtocol) (err error) { + if p.IsSetReferences() { + if err := oprot.WriteFieldBegin("references", thrift.LIST, 6); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 6:references: ", p), err) + } + if err := oprot.WriteListBegin(thrift.STRUCT, len(p.References)); err != nil { + return thrift.PrependError("error writing list begin: ", err) + } + for _, v := range p.References { + if err := v.Write(oprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", v), err) + } + } + if err := oprot.WriteListEnd(); err != nil { + return thrift.PrependError("error writing list end: ", err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 6:references: ", p), err) + } + } + return err +} + +func (p *Span) writeField7(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("flags", thrift.I32, 7); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 7:flags: ", p), err) + } + if err := oprot.WriteI32(int32(p.Flags)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.flags (7) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 7:flags: ", p), err) + } + return err +} + +func (p *Span) writeField8(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("startTime", thrift.I64, 8); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 8:startTime: ", p), err) + } + if err := oprot.WriteI64(int64(p.StartTime)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.startTime (8) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 8:startTime: ", p), err) + } + return err +} + +func (p *Span) writeField9(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("duration", thrift.I64, 9); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 9:duration: ", p), err) + } + if err := oprot.WriteI64(int64(p.Duration)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.duration (9) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 9:duration: ", p), err) + } + return err +} + +func (p *Span) writeField10(oprot thrift.TProtocol) (err error) { + if p.IsSetTags() { + if err := oprot.WriteFieldBegin("tags", thrift.LIST, 10); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 10:tags: ", p), err) + } + if err := oprot.WriteListBegin(thrift.STRUCT, len(p.Tags)); err != nil { + return thrift.PrependError("error writing list begin: ", err) + } + for _, v := range p.Tags { + if err := v.Write(oprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", v), err) + } + } + if err := oprot.WriteListEnd(); err != nil { + return thrift.PrependError("error writing list end: ", err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 10:tags: ", p), err) + } + } + return err +} + +func (p *Span) writeField11(oprot thrift.TProtocol) (err error) { + if p.IsSetLogs() { + if err := oprot.WriteFieldBegin("logs", thrift.LIST, 11); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 11:logs: ", p), err) + } + if err := oprot.WriteListBegin(thrift.STRUCT, len(p.Logs)); err != nil { + return thrift.PrependError("error writing list begin: ", err) + } + for _, v := range p.Logs { + if err := v.Write(oprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", v), err) + } + } + if err := oprot.WriteListEnd(); err != nil { + return thrift.PrependError("error writing list end: ", err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 11:logs: ", p), err) + } + } + return err +} + +func (p *Span) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("Span(%+v)", *p) +} + +// Attributes: +// - ServiceName +// - Tags +type Process struct { + ServiceName string `thrift:"serviceName,1,required" db:"serviceName" json:"serviceName"` + Tags []*Tag `thrift:"tags,2" db:"tags" json:"tags,omitempty"` +} + +func NewProcess() *Process { + return &Process{} +} + +func (p *Process) GetServiceName() string { + return p.ServiceName +} + +var Process_Tags_DEFAULT []*Tag + +func (p *Process) GetTags() []*Tag { + return p.Tags +} +func (p *Process) IsSetTags() bool { + return p.Tags != nil +} + +func (p *Process) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) + } + + var issetServiceName bool = false + + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if fieldTypeId == thrift.STRING { + if err := p.ReadField1(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetServiceName = true + case 2: + if fieldTypeId == thrift.LIST { + if err := p.ReadField2(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + default: + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + if err := iprot.ReadFieldEnd(); err != nil { + return err + } + } + if err := iprot.ReadStructEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) + } + if !issetServiceName { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field ServiceName is not set")) + } + return nil +} + +func (p *Process) ReadField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadString(); err != nil { + return thrift.PrependError("error reading field 1: ", err) + } else { + p.ServiceName = v + } + return nil +} + +func (p *Process) ReadField2(iprot thrift.TProtocol) error { + _, size, err := iprot.ReadListBegin() + if err != nil { + return thrift.PrependError("error reading list begin: ", err) + } + tSlice := make([]*Tag, 0, size) + p.Tags = tSlice + for i := 0; i < size; i++ { + _elem4 := &Tag{} + if err := _elem4.Read(iprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", _elem4), err) + } + p.Tags = append(p.Tags, _elem4) + } + if err := iprot.ReadListEnd(); err != nil { + return thrift.PrependError("error reading list end: ", err) + } + return nil +} + +func (p *Process) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("Process"); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) + } + if p != nil { + if err := p.writeField1(oprot); err != nil { + return err + } + if err := p.writeField2(oprot); err != nil { + return err + } + } + if err := oprot.WriteFieldStop(); err != nil { + return thrift.PrependError("write field stop error: ", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return thrift.PrependError("write struct stop error: ", err) + } + return nil +} + +func (p *Process) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("serviceName", thrift.STRING, 1); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:serviceName: ", p), err) + } + if err := oprot.WriteString(string(p.ServiceName)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.serviceName (1) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 1:serviceName: ", p), err) + } + return err +} + +func (p *Process) writeField2(oprot thrift.TProtocol) (err error) { + if p.IsSetTags() { + if err := oprot.WriteFieldBegin("tags", thrift.LIST, 2); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:tags: ", p), err) + } + if err := oprot.WriteListBegin(thrift.STRUCT, len(p.Tags)); err != nil { + return thrift.PrependError("error writing list begin: ", err) + } + for _, v := range p.Tags { + if err := v.Write(oprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", v), err) + } + } + if err := oprot.WriteListEnd(); err != nil { + return thrift.PrependError("error writing list end: ", err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 2:tags: ", p), err) + } + } + return err +} + +func (p *Process) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("Process(%+v)", *p) +} + +// Attributes: +// - Process +// - Spans +type Batch struct { + Process *Process `thrift:"process,1,required" db:"process" json:"process"` + Spans []*Span `thrift:"spans,2,required" db:"spans" json:"spans"` +} + +func NewBatch() *Batch { + return &Batch{} +} + +var Batch_Process_DEFAULT *Process + +func (p *Batch) GetProcess() *Process { + if !p.IsSetProcess() { + return Batch_Process_DEFAULT + } + return p.Process +} + +func (p *Batch) GetSpans() []*Span { + return p.Spans +} +func (p *Batch) IsSetProcess() bool { + return p.Process != nil +} + +func (p *Batch) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) + } + + var issetProcess bool = false + var issetSpans bool = false + + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if fieldTypeId == thrift.STRUCT { + if err := p.ReadField1(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetProcess = true + case 2: + if fieldTypeId == thrift.LIST { + if err := p.ReadField2(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetSpans = true + default: + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + if err := iprot.ReadFieldEnd(); err != nil { + return err + } + } + if err := iprot.ReadStructEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) + } + if !issetProcess { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field Process is not set")) + } + if !issetSpans { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field Spans is not set")) + } + return nil +} + +func (p *Batch) ReadField1(iprot thrift.TProtocol) error { + p.Process = &Process{} + if err := p.Process.Read(iprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", p.Process), err) + } + return nil +} + +func (p *Batch) ReadField2(iprot thrift.TProtocol) error { + _, size, err := iprot.ReadListBegin() + if err != nil { + return thrift.PrependError("error reading list begin: ", err) + } + tSlice := make([]*Span, 0, size) + p.Spans = tSlice + for i := 0; i < size; i++ { + _elem5 := &Span{} + if err := _elem5.Read(iprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", _elem5), err) + } + p.Spans = append(p.Spans, _elem5) + } + if err := iprot.ReadListEnd(); err != nil { + return thrift.PrependError("error reading list end: ", err) + } + return nil +} + +func (p *Batch) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("Batch"); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) + } + if p != nil { + if err := p.writeField1(oprot); err != nil { + return err + } + if err := p.writeField2(oprot); err != nil { + return err + } + } + if err := oprot.WriteFieldStop(); err != nil { + return thrift.PrependError("write field stop error: ", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return thrift.PrependError("write struct stop error: ", err) + } + return nil +} + +func (p *Batch) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("process", thrift.STRUCT, 1); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:process: ", p), err) + } + if err := p.Process.Write(oprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", p.Process), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 1:process: ", p), err) + } + return err +} + +func (p *Batch) writeField2(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("spans", thrift.LIST, 2); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:spans: ", p), err) + } + if err := oprot.WriteListBegin(thrift.STRUCT, len(p.Spans)); err != nil { + return thrift.PrependError("error writing list begin: ", err) + } + for _, v := range p.Spans { + if err := v.Write(oprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", v), err) + } + } + if err := oprot.WriteListEnd(); err != nil { + return thrift.PrependError("error writing list end: ", err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 2:spans: ", p), err) + } + return err +} + +func (p *Batch) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("Batch(%+v)", *p) +} + +// Attributes: +// - Ok +type BatchSubmitResponse struct { + Ok bool `thrift:"ok,1,required" db:"ok" json:"ok"` +} + +func NewBatchSubmitResponse() *BatchSubmitResponse { + return &BatchSubmitResponse{} +} + +func (p *BatchSubmitResponse) GetOk() bool { + return p.Ok +} +func (p *BatchSubmitResponse) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) + } + + var issetOk bool = false + + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if fieldTypeId == thrift.BOOL { + if err := p.ReadField1(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + issetOk = true + default: + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + if err := iprot.ReadFieldEnd(); err != nil { + return err + } + } + if err := iprot.ReadStructEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) + } + if !issetOk { + return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field Ok is not set")) + } + return nil +} + +func (p *BatchSubmitResponse) ReadField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadBool(); err != nil { + return thrift.PrependError("error reading field 1: ", err) + } else { + p.Ok = v + } + return nil +} + +func (p *BatchSubmitResponse) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("BatchSubmitResponse"); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) + } + if p != nil { + if err := p.writeField1(oprot); err != nil { + return err + } + } + if err := oprot.WriteFieldStop(); err != nil { + return thrift.PrependError("write field stop error: ", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return thrift.PrependError("write struct stop error: ", err) + } + return nil +} + +func (p *BatchSubmitResponse) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("ok", thrift.BOOL, 1); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:ok: ", p), err) + } + if err := oprot.WriteBool(bool(p.Ok)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.ok (1) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 1:ok: ", p), err) + } + return err +} + +func (p *BatchSubmitResponse) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("BatchSubmitResponse(%+v)", *p) +} + +type Collector interface { + // Parameters: + // - Batches + SubmitBatches(ctx context.Context, batches []*Batch) (r []*BatchSubmitResponse, err error) +} + +type CollectorClient struct { + c thrift.TClient +} + +// Deprecated: Use NewCollector instead +func NewCollectorClientFactory(t thrift.TTransport, f thrift.TProtocolFactory) *CollectorClient { + return &CollectorClient{ + c: thrift.NewTStandardClient(f.GetProtocol(t), f.GetProtocol(t)), + } +} + +// Deprecated: Use NewCollector instead +func NewCollectorClientProtocol(t thrift.TTransport, iprot thrift.TProtocol, oprot thrift.TProtocol) *CollectorClient { + return &CollectorClient{ + c: thrift.NewTStandardClient(iprot, oprot), + } +} + +func NewCollectorClient(c thrift.TClient) *CollectorClient { + return &CollectorClient{ + c: c, + } +} + +// Parameters: +// - Batches +func (p *CollectorClient) SubmitBatches(ctx context.Context, batches []*Batch) (r []*BatchSubmitResponse, err error) { + var _args6 CollectorSubmitBatchesArgs + _args6.Batches = batches + var _result7 CollectorSubmitBatchesResult + if err = p.c.Call(ctx, "submitBatches", &_args6, &_result7); err != nil { + return + } + return _result7.GetSuccess(), nil +} + +type CollectorProcessor struct { + processorMap map[string]thrift.TProcessorFunction + handler Collector +} + +func (p *CollectorProcessor) AddToProcessorMap(key string, processor thrift.TProcessorFunction) { + p.processorMap[key] = processor +} + +func (p *CollectorProcessor) GetProcessorFunction(key string) (processor thrift.TProcessorFunction, ok bool) { + processor, ok = p.processorMap[key] + return processor, ok +} + +func (p *CollectorProcessor) ProcessorMap() map[string]thrift.TProcessorFunction { + return p.processorMap +} + +func NewCollectorProcessor(handler Collector) *CollectorProcessor { + + self8 := &CollectorProcessor{handler: handler, processorMap: make(map[string]thrift.TProcessorFunction)} + self8.processorMap["submitBatches"] = &collectorProcessorSubmitBatches{handler: handler} + return self8 +} + +func (p *CollectorProcessor) Process(ctx context.Context, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + name, _, seqId, err := iprot.ReadMessageBegin() + if err != nil { + return false, err + } + if processor, ok := p.GetProcessorFunction(name); ok { + return processor.Process(ctx, seqId, iprot, oprot) + } + iprot.Skip(thrift.STRUCT) + iprot.ReadMessageEnd() + x9 := thrift.NewTApplicationException(thrift.UNKNOWN_METHOD, "Unknown function "+name) + oprot.WriteMessageBegin(name, thrift.EXCEPTION, seqId) + x9.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return false, x9 + +} + +type collectorProcessorSubmitBatches struct { + handler Collector +} + +func (p *collectorProcessorSubmitBatches) Process(ctx context.Context, seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + args := CollectorSubmitBatchesArgs{} + if err = args.Read(iprot); err != nil { + iprot.ReadMessageEnd() + x := thrift.NewTApplicationException(thrift.PROTOCOL_ERROR, err.Error()) + oprot.WriteMessageBegin("submitBatches", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return false, err + } + + iprot.ReadMessageEnd() + result := CollectorSubmitBatchesResult{} + var retval []*BatchSubmitResponse + var err2 error + if retval, err2 = p.handler.SubmitBatches(ctx, args.Batches); err2 != nil { + x := thrift.NewTApplicationException(thrift.INTERNAL_ERROR, "Internal error processing submitBatches: "+err2.Error()) + oprot.WriteMessageBegin("submitBatches", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return true, err2 + } else { + result.Success = retval + } + if err2 = oprot.WriteMessageBegin("submitBatches", thrift.REPLY, seqId); err2 != nil { + err = err2 + } + if err2 = result.Write(oprot); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.WriteMessageEnd(); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.Flush(ctx); err == nil && err2 != nil { + err = err2 + } + if err != nil { + return + } + return true, err +} + +// HELPER FUNCTIONS AND STRUCTURES + +// Attributes: +// - Batches +type CollectorSubmitBatchesArgs struct { + Batches []*Batch `thrift:"batches,1" db:"batches" json:"batches"` +} + +func NewCollectorSubmitBatchesArgs() *CollectorSubmitBatchesArgs { + return &CollectorSubmitBatchesArgs{} +} + +func (p *CollectorSubmitBatchesArgs) GetBatches() []*Batch { + return p.Batches +} +func (p *CollectorSubmitBatchesArgs) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) + } + + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if fieldTypeId == thrift.LIST { + if err := p.ReadField1(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + default: + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + if err := iprot.ReadFieldEnd(); err != nil { + return err + } + } + if err := iprot.ReadStructEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) + } + return nil +} + +func (p *CollectorSubmitBatchesArgs) ReadField1(iprot thrift.TProtocol) error { + _, size, err := iprot.ReadListBegin() + if err != nil { + return thrift.PrependError("error reading list begin: ", err) + } + tSlice := make([]*Batch, 0, size) + p.Batches = tSlice + for i := 0; i < size; i++ { + _elem10 := &Batch{} + if err := _elem10.Read(iprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", _elem10), err) + } + p.Batches = append(p.Batches, _elem10) + } + if err := iprot.ReadListEnd(); err != nil { + return thrift.PrependError("error reading list end: ", err) + } + return nil +} + +func (p *CollectorSubmitBatchesArgs) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("submitBatches_args"); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) + } + if p != nil { + if err := p.writeField1(oprot); err != nil { + return err + } + } + if err := oprot.WriteFieldStop(); err != nil { + return thrift.PrependError("write field stop error: ", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return thrift.PrependError("write struct stop error: ", err) + } + return nil +} + +func (p *CollectorSubmitBatchesArgs) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("batches", thrift.LIST, 1); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:batches: ", p), err) + } + if err := oprot.WriteListBegin(thrift.STRUCT, len(p.Batches)); err != nil { + return thrift.PrependError("error writing list begin: ", err) + } + for _, v := range p.Batches { + if err := v.Write(oprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", v), err) + } + } + if err := oprot.WriteListEnd(); err != nil { + return thrift.PrependError("error writing list end: ", err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 1:batches: ", p), err) + } + return err +} + +func (p *CollectorSubmitBatchesArgs) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("CollectorSubmitBatchesArgs(%+v)", *p) +} + +// Attributes: +// - Success +type CollectorSubmitBatchesResult struct { + Success []*BatchSubmitResponse `thrift:"success,0" db:"success" json:"success,omitempty"` +} + +func NewCollectorSubmitBatchesResult() *CollectorSubmitBatchesResult { + return &CollectorSubmitBatchesResult{} +} + +var CollectorSubmitBatchesResult_Success_DEFAULT []*BatchSubmitResponse + +func (p *CollectorSubmitBatchesResult) GetSuccess() []*BatchSubmitResponse { + return p.Success +} +func (p *CollectorSubmitBatchesResult) IsSetSuccess() bool { + return p.Success != nil +} + +func (p *CollectorSubmitBatchesResult) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) + } + + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 0: + if fieldTypeId == thrift.LIST { + if err := p.ReadField0(iprot); err != nil { + return err + } + } else { + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + default: + if err := iprot.Skip(fieldTypeId); err != nil { + return err + } + } + if err := iprot.ReadFieldEnd(); err != nil { + return err + } + } + if err := iprot.ReadStructEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) + } + return nil +} + +func (p *CollectorSubmitBatchesResult) ReadField0(iprot thrift.TProtocol) error { + _, size, err := iprot.ReadListBegin() + if err != nil { + return thrift.PrependError("error reading list begin: ", err) + } + tSlice := make([]*BatchSubmitResponse, 0, size) + p.Success = tSlice + for i := 0; i < size; i++ { + _elem11 := &BatchSubmitResponse{} + if err := _elem11.Read(iprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", _elem11), err) + } + p.Success = append(p.Success, _elem11) + } + if err := iprot.ReadListEnd(); err != nil { + return thrift.PrependError("error reading list end: ", err) + } + return nil +} + +func (p *CollectorSubmitBatchesResult) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("submitBatches_result"); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) + } + if p != nil { + if err := p.writeField0(oprot); err != nil { + return err + } + } + if err := oprot.WriteFieldStop(); err != nil { + return thrift.PrependError("write field stop error: ", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return thrift.PrependError("write struct stop error: ", err) + } + return nil +} + +func (p *CollectorSubmitBatchesResult) writeField0(oprot thrift.TProtocol) (err error) { + if p.IsSetSuccess() { + if err := oprot.WriteFieldBegin("success", thrift.LIST, 0); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 0:success: ", p), err) + } + if err := oprot.WriteListBegin(thrift.STRUCT, len(p.Success)); err != nil { + return thrift.PrependError("error writing list begin: ", err) + } + for _, v := range p.Success { + if err := v.Write(oprot); err != nil { + return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", v), err) + } + } + if err := oprot.WriteListEnd(); err != nil { + return thrift.PrependError("error writing list end: ", err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 0:success: ", p), err) + } + } + return err +} + +func (p *CollectorSubmitBatchesResult) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("CollectorSubmitBatchesResult(%+v)", *p) +} diff --git a/vendor/go.opencensus.io/exporter/jaeger/jaeger.go b/vendor/go.opencensus.io/exporter/jaeger/jaeger.go new file mode 100644 index 0000000000..d108cc0397 --- /dev/null +++ b/vendor/go.opencensus.io/exporter/jaeger/jaeger.go @@ -0,0 +1,340 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package jaeger contains an OpenCensus tracing exporter for Jaeger. +package jaeger // import "go.opencensus.io/exporter/jaeger" + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + + "git.apache.org/thrift.git/lib/go/thrift" + gen "go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger" + "go.opencensus.io/trace" + "google.golang.org/api/support/bundler" +) + +const defaultServiceName = "OpenCensus" + +// Options are the options to be used when initializing a Jaeger exporter. +type Options struct { + // Endpoint is the Jaeger HTTP Thrift endpoint. + // For example, http://localhost:14268. + // + // Deprecated: Use CollectorEndpoint instead. + Endpoint string + + // CollectorEndpoint is the full url to the Jaeger HTTP Thrift collector. + // For example, http://localhost:14268/api/traces + CollectorEndpoint string + + // AgentEndpoint instructs exporter to send spans to jaeger-agent at this address. + // For example, localhost:6831. + AgentEndpoint string + + // OnError is the hook to be called when there is + // an error occurred when uploading the stats data. + // If no custom hook is set, errors are logged. + // Optional. + OnError func(err error) + + // Username to be used if basic auth is required. + // Optional. + Username string + + // Password to be used if basic auth is required. + // Optional. + Password string + + // ServiceName is the Jaeger service name. + // Deprecated: Specify Process instead. + ServiceName string + + // Process contains the information about the exporting process. + Process Process +} + +// NewExporter returns a trace.Exporter implementation that exports +// the collected spans to Jaeger. +func NewExporter(o Options) (*Exporter, error) { + if o.Endpoint == "" && o.CollectorEndpoint == "" && o.AgentEndpoint == "" { + return nil, errors.New("missing endpoint for Jaeger exporter") + } + + var endpoint string + var client *agentClientUDP + var err error + if o.Endpoint != "" { + endpoint = o.Endpoint + "/api/traces?format=jaeger.thrift" + log.Printf("Endpoint has been deprecated. Please use CollectorEndpoint instead.") + } else if o.CollectorEndpoint != "" { + endpoint = o.CollectorEndpoint + } else { + client, err = newAgentClientUDP(o.AgentEndpoint, udpPacketMaxLength) + if err != nil { + return nil, err + } + } + onError := func(err error) { + if o.OnError != nil { + o.OnError(err) + return + } + log.Printf("Error when uploading spans to Jaeger: %v", err) + } + service := o.Process.ServiceName + if service == "" && o.ServiceName != "" { + // fallback to old service name if specified + service = o.ServiceName + } else if service == "" { + service = defaultServiceName + } + tags := make([]*gen.Tag, len(o.Process.Tags)) + for i, tag := range o.Process.Tags { + tags[i] = attributeToTag(tag.key, tag.value) + } + e := &Exporter{ + endpoint: endpoint, + agentEndpoint: o.AgentEndpoint, + client: client, + username: o.Username, + password: o.Password, + process: &gen.Process{ + ServiceName: service, + Tags: tags, + }, + } + bundler := bundler.NewBundler((*gen.Span)(nil), func(bundle interface{}) { + if err := e.upload(bundle.([]*gen.Span)); err != nil { + onError(err) + } + }) + e.bundler = bundler + return e, nil +} + +// Process contains the information exported to jaeger about the source +// of the trace data. +type Process struct { + // ServiceName is the Jaeger service name. + ServiceName string + + // Tags are added to Jaeger Process exports + Tags []Tag +} + +// Tag defines a key-value pair +// It is limited to the possible conversions to *jaeger.Tag by attributeToTag +type Tag struct { + key string + value interface{} +} + +// BoolTag creates a new tag of type bool, exported as jaeger.TagType_BOOL +func BoolTag(key string, value bool) Tag { + return Tag{key, value} +} + +// StringTag creates a new tag of type string, exported as jaeger.TagType_STRING +func StringTag(key string, value string) Tag { + return Tag{key, value} +} + +// Int64Tag creates a new tag of type int64, exported as jaeger.TagType_LONG +func Int64Tag(key string, value int64) Tag { + return Tag{key, value} +} + +// Exporter is an implementation of trace.Exporter that uploads spans to Jaeger. +type Exporter struct { + endpoint string + agentEndpoint string + process *gen.Process + bundler *bundler.Bundler + client *agentClientUDP + + username, password string +} + +var _ trace.Exporter = (*Exporter)(nil) + +// ExportSpan exports a SpanData to Jaeger. +func (e *Exporter) ExportSpan(data *trace.SpanData) { + e.bundler.Add(spanDataToThrift(data), 1) + // TODO(jbd): Handle oversized bundlers. +} + +func spanDataToThrift(data *trace.SpanData) *gen.Span { + tags := make([]*gen.Tag, 0, len(data.Attributes)) + for k, v := range data.Attributes { + tag := attributeToTag(k, v) + if tag != nil { + tags = append(tags, tag) + } + } + + tags = append(tags, + attributeToTag("status.code", data.Status.Code), + attributeToTag("status.message", data.Status.Message), + ) + + var logs []*gen.Log + for _, a := range data.Annotations { + fields := make([]*gen.Tag, 0, len(a.Attributes)) + for k, v := range a.Attributes { + tag := attributeToTag(k, v) + if tag != nil { + fields = append(fields, tag) + } + } + fields = append(fields, attributeToTag("message", a.Message)) + logs = append(logs, &gen.Log{ + Timestamp: a.Time.UnixNano() / 1000, + Fields: fields, + }) + } + var refs []*gen.SpanRef + for _, link := range data.Links { + refs = append(refs, &gen.SpanRef{ + TraceIdHigh: bytesToInt64(link.TraceID[0:8]), + TraceIdLow: bytesToInt64(link.TraceID[8:16]), + SpanId: bytesToInt64(link.SpanID[:]), + }) + } + return &gen.Span{ + TraceIdHigh: bytesToInt64(data.TraceID[0:8]), + TraceIdLow: bytesToInt64(data.TraceID[8:16]), + SpanId: bytesToInt64(data.SpanID[:]), + ParentSpanId: bytesToInt64(data.ParentSpanID[:]), + OperationName: name(data), + Flags: int32(data.TraceOptions), + StartTime: data.StartTime.UnixNano() / 1000, + Duration: data.EndTime.Sub(data.StartTime).Nanoseconds() / 1000, + Tags: tags, + Logs: logs, + References: refs, + } +} + +func name(sd *trace.SpanData) string { + n := sd.Name + switch sd.SpanKind { + case trace.SpanKindClient: + n = "Sent." + n + case trace.SpanKindServer: + n = "Recv." + n + } + return n +} + +func attributeToTag(key string, a interface{}) *gen.Tag { + var tag *gen.Tag + switch value := a.(type) { + case bool: + tag = &gen.Tag{ + Key: key, + VBool: &value, + VType: gen.TagType_BOOL, + } + case string: + tag = &gen.Tag{ + Key: key, + VStr: &value, + VType: gen.TagType_STRING, + } + case int64: + tag = &gen.Tag{ + Key: key, + VLong: &value, + VType: gen.TagType_LONG, + } + case int32: + v := int64(value) + tag = &gen.Tag{ + Key: key, + VLong: &v, + VType: gen.TagType_LONG, + } + } + return tag +} + +// Flush waits for exported trace spans to be uploaded. +// +// This is useful if your program is ending and you do not want to lose recent spans. +func (e *Exporter) Flush() { + e.bundler.Flush() +} + +func (e *Exporter) upload(spans []*gen.Span) error { + batch := &gen.Batch{ + Spans: spans, + Process: e.process, + } + if e.endpoint != "" { + return e.uploadCollector(batch) + } + return e.uploadAgent(batch) +} + +func (e *Exporter) uploadAgent(batch *gen.Batch) error { + return e.client.EmitBatch(batch) +} + +func (e *Exporter) uploadCollector(batch *gen.Batch) error { + body, err := serialize(batch) + if err != nil { + return err + } + req, err := http.NewRequest("POST", e.endpoint, body) + if err != nil { + return err + } + if e.username != "" && e.password != "" { + req.SetBasicAuth(e.username, e.password) + } + req.Header.Set("Content-Type", "application/x-thrift") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("failed to upload traces; HTTP status code: %d", resp.StatusCode) + } + return nil +} + +func serialize(obj thrift.TStruct) (*bytes.Buffer, error) { + buf := thrift.NewTMemoryBuffer() + if err := obj.Write(thrift.NewTBinaryProtocolTransport(buf)); err != nil { + return nil, err + } + return buf.Buffer, nil +} + +func bytesToInt64(buf []byte) int64 { + u := binary.BigEndian.Uint64(buf) + return int64(u) +} diff --git a/vendor/go.opencensus.io/exporter/jaeger/jaeger_test.go b/vendor/go.opencensus.io/exporter/jaeger/jaeger_test.go new file mode 100644 index 0000000000..5d5431da0b --- /dev/null +++ b/vendor/go.opencensus.io/exporter/jaeger/jaeger_test.go @@ -0,0 +1,135 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jaeger + +import ( + "fmt" + "reflect" + "testing" + "time" + + gen "go.opencensus.io/exporter/jaeger/internal/gen-go/jaeger" + "go.opencensus.io/trace" +) + +// TODO(jbd): Test export. + +func Test_bytesToInt64(t *testing.T) { + type args struct { + } + tests := []struct { + buf []byte + want int64 + }{ + { + buf: []byte{255, 0, 0, 0, 0, 0, 0, 0}, + want: -72057594037927936, + }, + { + buf: []byte{0, 0, 0, 0, 0, 0, 0, 1}, + want: 1, + }, + { + buf: []byte{0, 0, 0, 0, 0, 0, 0, 0}, + want: 0, + }, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("%d", tt.want), func(t *testing.T) { + if got := bytesToInt64(tt.buf); got != tt.want { + t.Errorf("bytesToInt64() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_spanDataToThrift(t *testing.T) { + now := time.Now() + + answerValue := int64(42) + keyValue := "value" + resultValue := true + statusCodeValue := int64(2) + statusMessage := "error" + + tests := []struct { + name string + data *trace.SpanData + want *gen.Span + }{ + { + name: "no parent", + data: &trace.SpanData{ + SpanContext: trace.SpanContext{ + TraceID: trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + SpanID: trace.SpanID{1, 2, 3, 4, 5, 6, 7, 8}, + }, + Name: "/foo", + StartTime: now, + EndTime: now, + Attributes: map[string]interface{}{ + "key": keyValue, + }, + Annotations: []trace.Annotation{ + { + Time: now, + Message: statusMessage, + Attributes: map[string]interface{}{ + "answer": answerValue, + }, + }, + { + Time: now, + Message: statusMessage, + Attributes: map[string]interface{}{ + "result": resultValue, + }, + }, + }, + Status: trace.Status{Code: trace.StatusCodeUnknown, Message: "error"}, + }, + want: &gen.Span{ + TraceIdLow: 651345242494996240, + TraceIdHigh: 72623859790382856, + SpanId: 72623859790382856, + OperationName: "/foo", + StartTime: now.UnixNano() / 1000, + Duration: 0, + Tags: []*gen.Tag{ + {Key: "key", VType: gen.TagType_STRING, VStr: &keyValue}, + {Key: "status.code", VType: gen.TagType_LONG, VLong: &statusCodeValue}, + {Key: "status.message", VType: gen.TagType_STRING, VStr: &statusMessage}, + }, + Logs: []*gen.Log{ + {Timestamp: now.UnixNano() / 1000, Fields: []*gen.Tag{ + {Key: "answer", VType: gen.TagType_LONG, VLong: &answerValue}, + {Key: "message", VType: gen.TagType_STRING, VStr: &statusMessage}, + }}, + {Timestamp: now.UnixNano() / 1000, Fields: []*gen.Tag{ + {Key: "result", VType: gen.TagType_BOOL, VBool: &resultValue}, + {Key: "message", VType: gen.TagType_STRING, VStr: &statusMessage}, + }}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := spanDataToThrift(tt.data); !reflect.DeepEqual(got, tt.want) { + t.Errorf("spanDataToThrift() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/vendor/go.opencensus.io/exporter/prometheus/example/main.go b/vendor/go.opencensus.io/exporter/prometheus/example/main.go new file mode 100644 index 0000000000..838cf36038 --- /dev/null +++ b/vendor/go.opencensus.io/exporter/prometheus/example/main.go @@ -0,0 +1,83 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Command prometheus is an example program that collects data for +// video size. Collected data is exported to Prometheus. +package main + +import ( + "context" + "log" + "math/rand" + "net/http" + "time" + + "go.opencensus.io/exporter/prometheus" + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" +) + +// Create measures. The program will record measures for the size of +// processed videos and the number of videos marked as spam. +var ( + videoCount = stats.Int64("example.com/measures/video_count", "number of processed videos", stats.UnitDimensionless) + videoSize = stats.Int64("example.com/measures/video_size", "size of processed video", stats.UnitBytes) +) + +func main() { + ctx := context.Background() + + exporter, err := prometheus.NewExporter(prometheus.Options{}) + if err != nil { + log.Fatal(err) + } + view.RegisterExporter(exporter) + + // Create view to see the number of processed videos cumulatively. + // Create view to see the amount of video processed + // Subscribe will allow view data to be exported. + // Once no longer needed, you can unsubscribe from the view. + if err = view.Register( + &view.View{ + Name: "video_count", + Description: "number of videos processed over time", + Measure: videoCount, + Aggregation: view.Count(), + }, + &view.View{ + Name: "video_size", + Description: "processed video size over time", + Measure: videoSize, + Aggregation: view.Distribution(0, 1<<16, 1<<32), + }, + ); err != nil { + log.Fatalf("Cannot register the view: %v", err) + } + + // Set reporting period to report data at every second. + view.SetReportingPeriod(1 * time.Second) + + // Record some data points... + go func() { + for { + stats.Record(ctx, videoCount.M(1), videoSize.M(rand.Int63())) + <-time.After(time.Millisecond * time.Duration(1+rand.Intn(400))) + } + }() + + addr := ":9999" + log.Printf("Serving at %s", addr) + http.Handle("/metrics", exporter) + log.Fatal(http.ListenAndServe(addr, nil)) +} diff --git a/vendor/go.opencensus.io/exporter/prometheus/example_test.go b/vendor/go.opencensus.io/exporter/prometheus/example_test.go new file mode 100644 index 0000000000..073a8bdd55 --- /dev/null +++ b/vendor/go.opencensus.io/exporter/prometheus/example_test.go @@ -0,0 +1,35 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus_test + +import ( + "log" + "net/http" + + "go.opencensus.io/exporter/prometheus" + "go.opencensus.io/stats/view" +) + +func Example() { + exporter, err := prometheus.NewExporter(prometheus.Options{}) + if err != nil { + log.Fatal(err) + } + view.RegisterExporter(exporter) + + // Serve the scrape endpoint on port 9999. + http.Handle("/metrics", exporter) + log.Fatal(http.ListenAndServe(":9999", nil)) +} diff --git a/vendor/go.opencensus.io/exporter/prometheus/prometheus.go b/vendor/go.opencensus.io/exporter/prometheus/prometheus.go new file mode 100644 index 0000000000..50665dcb1e --- /dev/null +++ b/vendor/go.opencensus.io/exporter/prometheus/prometheus.go @@ -0,0 +1,308 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package prometheus contains a Prometheus exporter that supports exporting +// OpenCensus views as Prometheus metrics. +package prometheus // import "go.opencensus.io/exporter/prometheus" + +import ( + "bytes" + "fmt" + "log" + "net/http" + "sort" + "sync" + + "go.opencensus.io/internal" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// Exporter exports stats to Prometheus, users need +// to register the exporter as an http.Handler to be +// able to export. +type Exporter struct { + opts Options + g prometheus.Gatherer + c *collector + handler http.Handler +} + +// Options contains options for configuring the exporter. +type Options struct { + Namespace string + Registry *prometheus.Registry + OnError func(err error) +} + +// NewExporter returns an exporter that exports stats to Prometheus. +func NewExporter(o Options) (*Exporter, error) { + if o.Registry == nil { + o.Registry = prometheus.NewRegistry() + } + collector := newCollector(o, o.Registry) + e := &Exporter{ + opts: o, + g: o.Registry, + c: collector, + handler: promhttp.HandlerFor(o.Registry, promhttp.HandlerOpts{}), + } + return e, nil +} + +var _ http.Handler = (*Exporter)(nil) +var _ view.Exporter = (*Exporter)(nil) + +func (c *collector) registerViews(views ...*view.View) { + count := 0 + for _, view := range views { + sig := viewSignature(c.opts.Namespace, view) + c.registeredViewsMu.Lock() + _, ok := c.registeredViews[sig] + c.registeredViewsMu.Unlock() + + if !ok { + desc := prometheus.NewDesc( + viewName(c.opts.Namespace, view), + view.Description, + tagKeysToLabels(view.TagKeys), + nil, + ) + c.registeredViewsMu.Lock() + c.registeredViews[sig] = desc + c.registeredViewsMu.Unlock() + count++ + } + } + if count == 0 { + return + } + + c.ensureRegisteredOnce() +} + +// ensureRegisteredOnce invokes reg.Register on the collector itself +// exactly once to ensure that we don't get errors such as +// cannot register the collector: descriptor Desc{fqName: *} +// already exists with the same fully-qualified name and const label values +// which is documented by Prometheus at +// https://github.com/prometheus/client_golang/blob/fcc130e101e76c5d303513d0e28f4b6d732845c7/prometheus/registry.go#L89-L101 +func (c *collector) ensureRegisteredOnce() { + c.registerOnce.Do(func() { + if err := c.reg.Register(c); err != nil { + c.opts.onError(fmt.Errorf("cannot register the collector: %v", err)) + } + }) + +} + +func (o *Options) onError(err error) { + if o.OnError != nil { + o.OnError(err) + } else { + log.Printf("Failed to export to Prometheus: %v", err) + } +} + +// ExportView exports to the Prometheus if view data has one or more rows. +// Each OpenCensus AggregationData will be converted to +// corresponding Prometheus Metric: SumData will be converted +// to Untyped Metric, CountData will be a Counter Metric, +// DistributionData will be a Histogram Metric. +func (e *Exporter) ExportView(vd *view.Data) { + if len(vd.Rows) == 0 { + return + } + e.c.addViewData(vd) +} + +// ServeHTTP serves the Prometheus endpoint. +func (e *Exporter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + e.handler.ServeHTTP(w, r) +} + +// collector implements prometheus.Collector +type collector struct { + opts Options + mu sync.Mutex // mu guards all the fields. + + registerOnce sync.Once + + // reg helps collector register views dynamically. + reg *prometheus.Registry + + // viewData are accumulated and atomically + // appended to on every Export invocation, from + // stats. These views are cleared out when + // Collect is invoked and the cycle is repeated. + viewData map[string]*view.Data + + registeredViewsMu sync.Mutex + // registeredViews maps a view to a prometheus desc. + registeredViews map[string]*prometheus.Desc +} + +func (c *collector) addViewData(vd *view.Data) { + c.registerViews(vd.View) + sig := viewSignature(c.opts.Namespace, vd.View) + + c.mu.Lock() + c.viewData[sig] = vd + c.mu.Unlock() +} + +func (c *collector) Describe(ch chan<- *prometheus.Desc) { + c.registeredViewsMu.Lock() + registered := make(map[string]*prometheus.Desc) + for k, desc := range c.registeredViews { + registered[k] = desc + } + c.registeredViewsMu.Unlock() + + for _, desc := range registered { + ch <- desc + } +} + +// Collect fetches the statistics from OpenCensus +// and delivers them as Prometheus Metrics. +// Collect is invoked everytime a prometheus.Gatherer is run +// for example when the HTTP endpoint is invoked by Prometheus. +func (c *collector) Collect(ch chan<- prometheus.Metric) { + // We need a copy of all the view data up until this point. + viewData := c.cloneViewData() + + for _, vd := range viewData { + sig := viewSignature(c.opts.Namespace, vd.View) + c.registeredViewsMu.Lock() + desc := c.registeredViews[sig] + c.registeredViewsMu.Unlock() + + for _, row := range vd.Rows { + metric, err := c.toMetric(desc, vd.View, row) + if err != nil { + c.opts.onError(err) + } else { + ch <- metric + } + } + } + +} + +func (c *collector) toMetric(desc *prometheus.Desc, v *view.View, row *view.Row) (prometheus.Metric, error) { + switch data := row.Data.(type) { + case *view.CountData: + return prometheus.NewConstMetric(desc, prometheus.CounterValue, float64(data.Value), tagValues(row.Tags)...) + + case *view.DistributionData: + points := make(map[float64]uint64) + // Histograms are cumulative in Prometheus. + // 1. Sort buckets in ascending order but, retain + // their indices for reverse lookup later on. + // TODO: If there is a guarantee that distribution elements + // are always sorted, then skip the sorting. + indicesMap := make(map[float64]int) + buckets := make([]float64, 0, len(v.Aggregation.Buckets)) + for i, b := range v.Aggregation.Buckets { + if _, ok := indicesMap[b]; !ok { + indicesMap[b] = i + buckets = append(buckets, b) + } + } + sort.Float64s(buckets) + + // 2. Now that the buckets are sorted by magnitude + // we can create cumulative indicesmap them back by reverse index + cumCount := uint64(0) + for _, b := range buckets { + i := indicesMap[b] + cumCount += uint64(data.CountPerBucket[i]) + points[b] = cumCount + } + return prometheus.NewConstHistogram(desc, uint64(data.Count), data.Sum(), points, tagValues(row.Tags)...) + + case *view.SumData: + return prometheus.NewConstMetric(desc, prometheus.UntypedValue, data.Value, tagValues(row.Tags)...) + + case *view.LastValueData: + return prometheus.NewConstMetric(desc, prometheus.GaugeValue, data.Value, tagValues(row.Tags)...) + + default: + return nil, fmt.Errorf("aggregation %T is not yet supported", v.Aggregation) + } +} + +func tagKeysToLabels(keys []tag.Key) (labels []string) { + for _, key := range keys { + labels = append(labels, internal.Sanitize(key.Name())) + } + return labels +} + +func tagsToLabels(tags []tag.Tag) []string { + var names []string + for _, tag := range tags { + names = append(names, internal.Sanitize(tag.Key.Name())) + } + return names +} + +func newCollector(opts Options, registrar *prometheus.Registry) *collector { + return &collector{ + reg: registrar, + opts: opts, + registeredViews: make(map[string]*prometheus.Desc), + viewData: make(map[string]*view.Data), + } +} + +func tagValues(t []tag.Tag) []string { + var values []string + for _, t := range t { + values = append(values, t.Value) + } + return values +} + +func viewName(namespace string, v *view.View) string { + var name string + if namespace != "" { + name = namespace + "_" + } + return name + internal.Sanitize(v.Name) +} + +func viewSignature(namespace string, v *view.View) string { + var buf bytes.Buffer + buf.WriteString(viewName(namespace, v)) + for _, k := range v.TagKeys { + buf.WriteString("-" + k.Name()) + } + return buf.String() +} + +func (c *collector) cloneViewData() map[string]*view.Data { + c.mu.Lock() + defer c.mu.Unlock() + + viewDataCopy := make(map[string]*view.Data) + for sig, viewData := range c.viewData { + viewDataCopy[sig] = viewData + } + return viewDataCopy +} diff --git a/vendor/go.opencensus.io/exporter/prometheus/prometheus_test.go b/vendor/go.opencensus.io/exporter/prometheus/prometheus_test.go new file mode 100644 index 0000000000..69985209f6 --- /dev/null +++ b/vendor/go.opencensus.io/exporter/prometheus/prometheus_test.go @@ -0,0 +1,344 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + "time" + + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" + + "github.com/prometheus/client_golang/prometheus" +) + +func newView(measureName string, agg *view.Aggregation) *view.View { + m := stats.Int64(measureName, "bytes", stats.UnitBytes) + return &view.View{ + Name: "foo", + Description: "bar", + Measure: m, + Aggregation: agg, + } +} + +func TestOnlyCumulativeWindowSupported(t *testing.T) { + // See Issue https://github.com/census-instrumentation/opencensus-go/issues/214. + count1 := &view.CountData{Value: 1} + lastValue1 := &view.LastValueData{Value: 56.7} + tests := []struct { + vds *view.Data + want int + }{ + 0: { + vds: &view.Data{ + View: newView("TestOnlyCumulativeWindowSupported/m1", view.Count()), + }, + want: 0, // no rows present + }, + 1: { + vds: &view.Data{ + View: newView("TestOnlyCumulativeWindowSupported/m2", view.Count()), + Rows: []*view.Row{ + {Data: count1}, + }, + }, + want: 1, + }, + 2: { + vds: &view.Data{ + View: newView("TestOnlyCumulativeWindowSupported/m3", view.LastValue()), + Rows: []*view.Row{ + {Data: lastValue1}, + }, + }, + want: 1, + }, + } + + for i, tt := range tests { + reg := prometheus.NewRegistry() + collector := newCollector(Options{}, reg) + collector.addViewData(tt.vds) + mm, err := reg.Gather() + if err != nil { + t.Errorf("#%d: Gather err: %v", i, err) + } + reg.Unregister(collector) + if got, want := len(mm), tt.want; got != want { + t.Errorf("#%d: got nil %v want nil %v", i, got, want) + } + } +} + +func TestCollectNonRacy(t *testing.T) { + // Despite enforcing the singleton, for this case we + // need an exporter hence won't be using NewExporter. + exp, err := NewExporter(Options{}) + if err != nil { + t.Fatalf("NewExporter: %v", err) + } + collector := exp.c + + // Synchronize and make sure every goroutine has terminated before we exit + var waiter sync.WaitGroup + waiter.Add(3) + defer waiter.Wait() + + doneCh := make(chan bool) + // 1. Viewdata write routine at 700ns + go func() { + defer waiter.Done() + tick := time.NewTicker(700 * time.Nanosecond) + defer tick.Stop() + + defer func() { + close(doneCh) + }() + + for i := 0; i < 1e3; i++ { + count1 := &view.CountData{Value: 1} + vds := []*view.Data{ + {View: newView(fmt.Sprintf("TestCollectNonRacy/m2-%d", i), view.Count()), Rows: []*view.Row{{Data: count1}}}, + } + for _, v := range vds { + exp.ExportView(v) + } + <-tick.C + } + }() + + inMetricsChan := make(chan prometheus.Metric, 1000) + // 2. Simulating the Prometheus metrics consumption routine running at 900ns + go func() { + defer waiter.Done() + tick := time.NewTicker(900 * time.Nanosecond) + defer tick.Stop() + + for { + select { + case <-doneCh: + return + case <-inMetricsChan: + } + } + }() + + // 3. Collect/Read routine at 800ns + go func() { + defer waiter.Done() + tick := time.NewTicker(800 * time.Nanosecond) + defer tick.Stop() + + for { + select { + case <-doneCh: + return + case <-tick.C: + // Perform some collection here + collector.Collect(inMetricsChan) + } + } + }() +} + +type mSlice []*stats.Int64Measure + +func (measures *mSlice) createAndAppend(name, desc, unit string) { + m := stats.Int64(name, desc, unit) + *measures = append(*measures, m) +} + +type vCreator []*view.View + +func (vc *vCreator) createAndAppend(name, description string, keys []tag.Key, measure stats.Measure, agg *view.Aggregation) { + v := &view.View{ + Name: name, + Description: description, + TagKeys: keys, + Measure: measure, + Aggregation: agg, + } + *vc = append(*vc, v) +} + +func TestMetricsEndpointOutput(t *testing.T) { + exporter, err := NewExporter(Options{}) + if err != nil { + t.Fatalf("failed to create prometheus exporter: %v", err) + } + view.RegisterExporter(exporter) + + names := []string{"foo", "bar", "baz"} + + var measures mSlice + for _, name := range names { + measures.createAndAppend("tests/"+name, name, "") + } + + var vc vCreator + for _, m := range measures { + vc.createAndAppend(m.Name(), m.Description(), nil, m, view.Count()) + } + + if err := view.Register(vc...); err != nil { + t.Fatalf("failed to create views: %v", err) + } + defer view.Unregister(vc...) + + view.SetReportingPeriod(time.Millisecond) + + for _, m := range measures { + stats.Record(context.Background(), m.M(1)) + } + + srv := httptest.NewServer(exporter) + defer srv.Close() + + var i int + var output string + for { + time.Sleep(10 * time.Millisecond) + if i == 1000 { + t.Fatal("no output at /metrics (10s wait)") + } + i++ + + resp, err := http.Get(srv.URL) + if err != nil { + t.Fatalf("failed to get /metrics: %v", err) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read body: %v", err) + } + resp.Body.Close() + + output = string(body) + if output != "" { + break + } + } + + if strings.Contains(output, "collected before with the same name and label values") { + t.Fatal("metric name and labels being duplicated but must be unique") + } + + if strings.Contains(output, "error(s) occurred") { + t.Fatal("error reported by prometheus registry") + } + + for _, name := range names { + if !strings.Contains(output, "tests_"+name+" 1") { + t.Fatalf("measurement missing in output: %v", name) + } + } +} + +func TestCumulativenessFromHistograms(t *testing.T) { + exporter, err := NewExporter(Options{}) + if err != nil { + t.Fatalf("failed to create prometheus exporter: %v", err) + } + view.RegisterExporter(exporter) + reportPeriod := time.Millisecond + view.SetReportingPeriod(reportPeriod) + + m := stats.Float64("tests/bills", "payments by denomination", stats.UnitDimensionless) + v := &view.View{ + Name: "cash/register", + Description: "this is a test", + Measure: m, + + // Intentionally used repeated elements in the ascending distribution. + // to ensure duplicate distribution items are handles. + Aggregation: view.Distribution(1, 5, 5, 5, 5, 10, 20, 50, 100, 250), + } + + if err := view.Register(v); err != nil { + t.Fatalf("Register error: %v", err) + } + defer view.Unregister(v) + + // Give the reporter ample time to process registration + <-time.After(10 * reportPeriod) + + values := []float64{0.25, 245.67, 12, 1.45, 199.9, 7.69, 187.12} + // We want the results that look like this: + // 1: [0.25] | 1 + prev(i) = 1 + 0 = 1 + // 5: [1.45] | 1 + prev(i) = 1 + 1 = 2 + // 10: [] | 1 + prev(i) = 1 + 2 = 3 + // 20: [12] | 1 + prev(i) = 1 + 3 = 4 + // 50: [] | 0 + prev(i) = 0 + 4 = 4 + // 100: [] | 0 + prev(i) = 0 + 4 = 4 + // 250: [187.12, 199.9, 245.67] | 3 + prev(i) = 3 + 4 = 7 + wantLines := []string{ + `cash_register_bucket{le="1"} 1`, + `cash_register_bucket{le="5"} 2`, + `cash_register_bucket{le="10"} 3`, + `cash_register_bucket{le="20"} 4`, + `cash_register_bucket{le="50"} 4`, + `cash_register_bucket{le="100"} 4`, + `cash_register_bucket{le="250"} 7`, + `cash_register_bucket{le="+Inf"} 7`, + `cash_register_sum 654.0799999999999`, // Summation of the input values + `cash_register_count 7`, + } + + ctx := context.Background() + ms := make([]stats.Measurement, 0, len(values)) + for _, value := range values { + mx := m.M(value) + ms = append(ms, mx) + } + stats.Record(ctx, ms...) + + // Give the recorder ample time to process recording + <-time.After(10 * reportPeriod) + + cst := httptest.NewServer(exporter) + defer cst.Close() + res, err := http.Get(cst.URL) + if err != nil { + t.Fatalf("http.Get error: %v", err) + } + blob, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("Read body error: %v", err) + } + str := strings.Trim(string(blob), "\n") + lines := strings.Split(str, "\n") + nonComments := make([]string, 0, len(lines)) + for _, line := range lines { + if !strings.Contains(line, "#") { + nonComments = append(nonComments, line) + } + } + + got := strings.Join(nonComments, "\n") + want := strings.Join(wantLines, "\n") + if got != want { + t.Fatalf("\ngot:\n%s\n\nwant:\n%s\n", got, want) + } +} diff --git a/vendor/go.opencensus.io/exporter/stackdriver/propagation/http.go b/vendor/go.opencensus.io/exporter/stackdriver/propagation/http.go new file mode 100644 index 0000000000..7cc02a1104 --- /dev/null +++ b/vendor/go.opencensus.io/exporter/stackdriver/propagation/http.go @@ -0,0 +1,94 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package propagation implement X-Cloud-Trace-Context header propagation used +// by Google Cloud products. +package propagation // import "go.opencensus.io/exporter/stackdriver/propagation" + +import ( + "encoding/binary" + "encoding/hex" + "fmt" + "net/http" + "strconv" + "strings" + + "go.opencensus.io/trace" + "go.opencensus.io/trace/propagation" +) + +const ( + httpHeaderMaxSize = 200 + httpHeader = `X-Cloud-Trace-Context` +) + +var _ propagation.HTTPFormat = (*HTTPFormat)(nil) + +// HTTPFormat implements propagation.HTTPFormat to propagate +// traces in HTTP headers for Google Cloud Platform and Stackdriver Trace. +type HTTPFormat struct{} + +// SpanContextFromRequest extracts a Stackdriver Trace span context from incoming requests. +func (f *HTTPFormat) SpanContextFromRequest(req *http.Request) (sc trace.SpanContext, ok bool) { + h := req.Header.Get(httpHeader) + // See https://cloud.google.com/trace/docs/faq for the header HTTPFormat. + // Return if the header is empty or missing, or if the header is unreasonably + // large, to avoid making unnecessary copies of a large string. + if h == "" || len(h) > httpHeaderMaxSize { + return trace.SpanContext{}, false + } + + // Parse the trace id field. + slash := strings.Index(h, `/`) + if slash == -1 { + return trace.SpanContext{}, false + } + tid, h := h[:slash], h[slash+1:] + + buf, err := hex.DecodeString(tid) + if err != nil { + return trace.SpanContext{}, false + } + copy(sc.TraceID[:], buf) + + // Parse the span id field. + spanstr := h + semicolon := strings.Index(h, `;`) + if semicolon != -1 { + spanstr, h = h[:semicolon], h[semicolon+1:] + } + sid, err := strconv.ParseUint(spanstr, 10, 64) + if err != nil { + return trace.SpanContext{}, false + } + binary.BigEndian.PutUint64(sc.SpanID[:], sid) + + // Parse the options field, options field is optional. + if !strings.HasPrefix(h, "o=") { + return sc, true + } + o, err := strconv.ParseUint(h[2:], 10, 64) + if err != nil { + return trace.SpanContext{}, false + } + sc.TraceOptions = trace.TraceOptions(o) + return sc, true +} + +// SpanContextToRequest modifies the given request to include a Stackdriver Trace header. +func (f *HTTPFormat) SpanContextToRequest(sc trace.SpanContext, req *http.Request) { + sid := binary.BigEndian.Uint64(sc.SpanID[:]) + header := fmt.Sprintf("%s/%d;o=%d", hex.EncodeToString(sc.TraceID[:]), sid, int64(sc.TraceOptions)) + req.Header.Set(httpHeader, header) +} diff --git a/vendor/go.opencensus.io/exporter/stackdriver/propagation/http_test.go b/vendor/go.opencensus.io/exporter/stackdriver/propagation/http_test.go new file mode 100644 index 0000000000..9ad93b714a --- /dev/null +++ b/vendor/go.opencensus.io/exporter/stackdriver/propagation/http_test.go @@ -0,0 +1,70 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package propagation + +import ( + "net/http" + "reflect" + "testing" + + "go.opencensus.io/trace" +) + +func TestHTTPFormat(t *testing.T) { + format := &HTTPFormat{} + traceID := [16]byte{16, 84, 69, 170, 120, 67, 188, 139, 242, 6, 177, 32, 0, 16, 0, 0} + spanID1 := [8]byte{255, 0, 0, 0, 0, 0, 0, 123} + spanID2 := [8]byte{0, 0, 0, 0, 0, 0, 0, 123} + tests := []struct { + incoming string + wantSpanContext trace.SpanContext + }{ + { + incoming: "105445aa7843bc8bf206b12000100000/18374686479671623803;o=1", + wantSpanContext: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID1, + TraceOptions: 1, + }, + }, + { + incoming: "105445aa7843bc8bf206b12000100000/123;o=0", + wantSpanContext: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID2, + TraceOptions: 0, + }, + }, + } + for _, tt := range tests { + t.Run(tt.incoming, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Add(httpHeader, tt.incoming) + sc, ok := format.SpanContextFromRequest(req) + if !ok { + t.Errorf("exporter.SpanContextFromRequest() = false; want true") + } + if got, want := sc, tt.wantSpanContext; !reflect.DeepEqual(got, want) { + t.Errorf("exporter.SpanContextFromRequest() returned span context %v; want %v", got, want) + } + + req, _ = http.NewRequest("GET", "http://example.com", nil) + format.SpanContextToRequest(sc, req) + if got, want := req.Header.Get(httpHeader), tt.incoming; got != want { + t.Errorf("exporter.SpanContextToRequest() returned header %q; want %q", got, want) + } + }) + } +} diff --git a/vendor/go.opencensus.io/exporter/zipkin/example/main.go b/vendor/go.opencensus.io/exporter/zipkin/example/main.go new file mode 100644 index 0000000000..9466c9809d --- /dev/null +++ b/vendor/go.opencensus.io/exporter/zipkin/example/main.go @@ -0,0 +1,77 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "log" + "time" + + openzipkin "github.com/openzipkin/zipkin-go" + "github.com/openzipkin/zipkin-go/reporter/http" + "go.opencensus.io/exporter/zipkin" + "go.opencensus.io/trace" +) + +func main() { + // The localEndpoint stores the name and address of the local service + localEndpoint, err := openzipkin.NewEndpoint("example-server", "192.168.1.5:5454") + if err != nil { + log.Println(err) + } + + // The Zipkin reporter takes collected spans from the app and reports them to the backend + // http://localhost:9411/api/v2/spans is the default for the Zipkin Span v2 + reporter := http.NewReporter("http://localhost:9411/api/v2/spans") + defer reporter.Close() + + // The OpenCensus exporter wraps the Zipkin reporter + exporter := zipkin.NewExporter(reporter, localEndpoint) + trace.RegisterExporter(exporter) + + // For example purposes, sample every trace. In a production application, you should + // configure this to a trace.ProbabilitySampler set at the desired + // probability. + trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) + + ctx := context.Background() + foo(ctx) +} + +func foo(ctx context.Context) { + // Name the current span "/foo" + ctx, span := trace.StartSpan(ctx, "/foo") + defer span.End() + + // Foo calls bar and baz + bar(ctx) + baz(ctx) +} + +func bar(ctx context.Context) { + ctx, span := trace.StartSpan(ctx, "/bar") + defer span.End() + + // Do bar + time.Sleep(2 * time.Millisecond) +} + +func baz(ctx context.Context) { + ctx, span := trace.StartSpan(ctx, "/baz") + defer span.End() + + // Do baz + time.Sleep(4 * time.Millisecond) +} diff --git a/vendor/go.opencensus.io/exporter/zipkin/example_test.go b/vendor/go.opencensus.io/exporter/zipkin/example_test.go new file mode 100644 index 0000000000..7ef147014b --- /dev/null +++ b/vendor/go.opencensus.io/exporter/zipkin/example_test.go @@ -0,0 +1,40 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zipkin_test + +import ( + "log" + + openzipkin "github.com/openzipkin/zipkin-go" + "github.com/openzipkin/zipkin-go/reporter/http" + "go.opencensus.io/exporter/zipkin" + "go.opencensus.io/trace" +) + +func Example() { + // import ( + // openzipkin "github.com/openzipkin/zipkin-go" + // "github.com/openzipkin/zipkin-go/reporter/http" + // "go.opencensus.io/exporter/trace/zipkin" + // ) + + localEndpoint, err := openzipkin.NewEndpoint("server", "192.168.1.5:5454") + if err != nil { + log.Print(err) + } + reporter := http.NewReporter("http://localhost:9411/api/v2/spans") + exporter := zipkin.NewExporter(reporter, localEndpoint) + trace.RegisterExporter(exporter) +} diff --git a/vendor/go.opencensus.io/exporter/zipkin/zipkin.go b/vendor/go.opencensus.io/exporter/zipkin/zipkin.go new file mode 100644 index 0000000000..30d2fa4380 --- /dev/null +++ b/vendor/go.opencensus.io/exporter/zipkin/zipkin.go @@ -0,0 +1,194 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package zipkin contains an trace exporter for Zipkin. +package zipkin // import "go.opencensus.io/exporter/zipkin" + +import ( + "encoding/binary" + "strconv" + + "github.com/openzipkin/zipkin-go/model" + "github.com/openzipkin/zipkin-go/reporter" + "go.opencensus.io/trace" +) + +// Exporter is an implementation of trace.Exporter that uploads spans to a +// Zipkin server. +type Exporter struct { + reporter reporter.Reporter + localEndpoint *model.Endpoint +} + +// NewExporter returns an implementation of trace.Exporter that uploads spans +// to a Zipkin server. +// +// reporter is a Zipkin Reporter which will be used to send the spans. These +// can be created with the openzipkin library, using one of the packages under +// github.com/openzipkin/zipkin-go/reporter. +// +// localEndpoint sets the local endpoint of exported spans. It can be +// constructed with github.com/openzipkin/zipkin-go.NewEndpoint, e.g.: +// localEndpoint, err := NewEndpoint("my server", listener.Addr().String()) +// localEndpoint can be nil. +func NewExporter(reporter reporter.Reporter, localEndpoint *model.Endpoint) *Exporter { + return &Exporter{ + reporter: reporter, + localEndpoint: localEndpoint, + } +} + +// ExportSpan exports a span to a Zipkin server. +func (e *Exporter) ExportSpan(s *trace.SpanData) { + e.reporter.Send(zipkinSpan(s, e.localEndpoint)) +} + +const ( + statusCodeTagKey = "error" + statusDescriptionTagKey = "opencensus.status_description" +) + +var ( + sampledTrue = true + canonicalCodes = [...]string{ + "OK", + "CANCELLED", + "UNKNOWN", + "INVALID_ARGUMENT", + "DEADLINE_EXCEEDED", + "NOT_FOUND", + "ALREADY_EXISTS", + "PERMISSION_DENIED", + "RESOURCE_EXHAUSTED", + "FAILED_PRECONDITION", + "ABORTED", + "OUT_OF_RANGE", + "UNIMPLEMENTED", + "INTERNAL", + "UNAVAILABLE", + "DATA_LOSS", + "UNAUTHENTICATED", + } +) + +func canonicalCodeString(code int32) string { + if code < 0 || int(code) >= len(canonicalCodes) { + return "error code " + strconv.FormatInt(int64(code), 10) + } + return canonicalCodes[code] +} + +func convertTraceID(t trace.TraceID) model.TraceID { + return model.TraceID{ + High: binary.BigEndian.Uint64(t[:8]), + Low: binary.BigEndian.Uint64(t[8:]), + } +} + +func convertSpanID(s trace.SpanID) model.ID { + return model.ID(binary.BigEndian.Uint64(s[:])) +} + +func spanKind(s *trace.SpanData) model.Kind { + switch s.SpanKind { + case trace.SpanKindClient: + return model.Client + case trace.SpanKindServer: + return model.Server + } + return model.Undetermined +} + +func zipkinSpan(s *trace.SpanData, localEndpoint *model.Endpoint) model.SpanModel { + sc := s.SpanContext + z := model.SpanModel{ + SpanContext: model.SpanContext{ + TraceID: convertTraceID(sc.TraceID), + ID: convertSpanID(sc.SpanID), + Sampled: &sampledTrue, + }, + Kind: spanKind(s), + Name: s.Name, + Timestamp: s.StartTime, + Shared: false, + LocalEndpoint: localEndpoint, + } + + if s.ParentSpanID != (trace.SpanID{}) { + id := convertSpanID(s.ParentSpanID) + z.ParentID = &id + } + + if s, e := s.StartTime, s.EndTime; !s.IsZero() && !e.IsZero() { + z.Duration = e.Sub(s) + } + + // construct Tags from s.Attributes and s.Status. + if len(s.Attributes) != 0 { + m := make(map[string]string, len(s.Attributes)+2) + for key, value := range s.Attributes { + switch v := value.(type) { + case string: + m[key] = v + case bool: + if v { + m[key] = "true" + } else { + m[key] = "false" + } + case int64: + m[key] = strconv.FormatInt(v, 10) + } + } + z.Tags = m + } + if s.Status.Code != 0 || s.Status.Message != "" { + if z.Tags == nil { + z.Tags = make(map[string]string, 2) + } + if s.Status.Code != 0 { + z.Tags[statusCodeTagKey] = canonicalCodeString(s.Status.Code) + } + if s.Status.Message != "" { + z.Tags[statusDescriptionTagKey] = s.Status.Message + } + } + + // construct Annotations from s.Annotations and s.MessageEvents. + if len(s.Annotations) != 0 || len(s.MessageEvents) != 0 { + z.Annotations = make([]model.Annotation, 0, len(s.Annotations)+len(s.MessageEvents)) + for _, a := range s.Annotations { + z.Annotations = append(z.Annotations, model.Annotation{ + Timestamp: a.Time, + Value: a.Message, + }) + } + for _, m := range s.MessageEvents { + a := model.Annotation{ + Timestamp: m.Time, + } + switch m.EventType { + case trace.MessageEventTypeSent: + a.Value = "SENT" + case trace.MessageEventTypeRecv: + a.Value = "RECV" + default: + a.Value = "" + } + z.Annotations = append(z.Annotations, a) + } + } + + return z +} diff --git a/vendor/go.opencensus.io/exporter/zipkin/zipkin_test.go b/vendor/go.opencensus.io/exporter/zipkin/zipkin_test.go new file mode 100644 index 0000000000..2d5f81cc1e --- /dev/null +++ b/vendor/go.opencensus.io/exporter/zipkin/zipkin_test.go @@ -0,0 +1,255 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zipkin + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "reflect" + "strings" + "testing" + "time" + + "github.com/openzipkin/zipkin-go/model" + httpreporter "github.com/openzipkin/zipkin-go/reporter/http" + "go.opencensus.io/trace" +) + +type roundTripper func(*http.Request) (*http.Response, error) + +func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return r(req) +} + +func TestExport(t *testing.T) { + // Since Zipkin reports in microsecond resolution let's round our Timestamp, + // so when deserializing Zipkin data in this test we can properly compare. + now := time.Now().Round(time.Microsecond) + tests := []struct { + span *trace.SpanData + want model.SpanModel + }{ + { + span: &trace.SpanData{ + SpanContext: trace.SpanContext{ + TraceID: trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + SpanID: trace.SpanID{17, 18, 19, 20, 21, 22, 23, 24}, + TraceOptions: 1, + }, + Name: "name", + SpanKind: trace.SpanKindClient, + StartTime: now, + EndTime: now.Add(24 * time.Hour), + Attributes: map[string]interface{}{ + "stringkey": "value", + "intkey": int64(42), + "boolkey1": true, + "boolkey2": false, + }, + MessageEvents: []trace.MessageEvent{ + { + Time: now, + EventType: trace.MessageEventTypeSent, + MessageID: 12, + UncompressedByteSize: 99, + CompressedByteSize: 98, + }, + }, + Annotations: []trace.Annotation{ + { + Time: now, + Message: "Annotation", + Attributes: map[string]interface{}{ + "stringkey": "value", + "intkey": int64(42), + "boolkey1": true, + "boolkey2": false, + }, + }, + }, + Status: trace.Status{ + Code: 3, + Message: "error", + }, + }, + want: model.SpanModel{ + SpanContext: model.SpanContext{ + TraceID: model.TraceID{ + High: 0x0102030405060708, + Low: 0x090a0b0c0d0e0f10, + }, + ID: 0x1112131415161718, + Sampled: &sampledTrue, + }, + Name: "name", + Kind: model.Client, + Timestamp: now, + Duration: 24 * time.Hour, + Shared: false, + Annotations: []model.Annotation{ + { + Timestamp: now, + Value: "Annotation", + }, + { + Timestamp: now, + Value: "SENT", + }, + }, + Tags: map[string]string{ + "stringkey": "value", + "intkey": "42", + "boolkey1": "true", + "boolkey2": "false", + "error": "INVALID_ARGUMENT", + "opencensus.status_description": "error", + }, + }, + }, + { + span: &trace.SpanData{ + SpanContext: trace.SpanContext{ + TraceID: trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + SpanID: trace.SpanID{17, 18, 19, 20, 21, 22, 23, 24}, + TraceOptions: 1, + }, + Name: "name", + StartTime: now, + EndTime: now.Add(24 * time.Hour), + }, + want: model.SpanModel{ + SpanContext: model.SpanContext{ + TraceID: model.TraceID{ + High: 0x0102030405060708, + Low: 0x090a0b0c0d0e0f10, + }, + ID: 0x1112131415161718, + Sampled: &sampledTrue, + }, + Name: "name", + Timestamp: now, + Duration: 24 * time.Hour, + Shared: false, + }, + }, + { + span: &trace.SpanData{ + SpanContext: trace.SpanContext{ + TraceID: trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + SpanID: trace.SpanID{17, 18, 19, 20, 21, 22, 23, 24}, + TraceOptions: 1, + }, + Name: "name", + StartTime: now, + EndTime: now.Add(24 * time.Hour), + Status: trace.Status{ + Code: 0, + Message: "there is no cause for alarm", + }, + }, + want: model.SpanModel{ + SpanContext: model.SpanContext{ + TraceID: model.TraceID{ + High: 0x0102030405060708, + Low: 0x090a0b0c0d0e0f10, + }, + ID: 0x1112131415161718, + Sampled: &sampledTrue, + }, + Name: "name", + Timestamp: now, + Duration: 24 * time.Hour, + Shared: false, + Tags: map[string]string{ + "opencensus.status_description": "there is no cause for alarm", + }, + }, + }, + { + span: &trace.SpanData{ + SpanContext: trace.SpanContext{ + TraceID: trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + SpanID: trace.SpanID{17, 18, 19, 20, 21, 22, 23, 24}, + TraceOptions: 1, + }, + Name: "name", + StartTime: now, + EndTime: now.Add(24 * time.Hour), + Status: trace.Status{ + Code: 1234, + }, + }, + want: model.SpanModel{ + SpanContext: model.SpanContext{ + TraceID: model.TraceID{ + High: 0x0102030405060708, + Low: 0x090a0b0c0d0e0f10, + }, + ID: 0x1112131415161718, + Sampled: &sampledTrue, + }, + Name: "name", + Timestamp: now, + Duration: 24 * time.Hour, + Shared: false, + Tags: map[string]string{ + "error": "error code 1234", + }, + }, + }, + } + for _, tt := range tests { + got := zipkinSpan(tt.span, nil) + if len(got.Annotations) != len(tt.want.Annotations) { + t.Fatalf("zipkinSpan: got %d annotations in span, want %d", len(got.Annotations), len(tt.want.Annotations)) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("zipkinSpan:\n\tgot %#v\n\twant %#v", got, tt.want) + } + } + for _, tt := range tests { + ch := make(chan []byte) + client := http.Client{ + Transport: roundTripper(func(req *http.Request) (*http.Response, error) { + body, _ := ioutil.ReadAll(req.Body) + ch <- body + return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader(""))}, nil + }), + } + reporter := httpreporter.NewReporter("foo", httpreporter.Client(&client), httpreporter.BatchInterval(time.Millisecond)) + exporter := NewExporter(reporter, nil) + exporter.ExportSpan(tt.span) + var data []byte + select { + case data = <-ch: + case <-time.After(2 * time.Second): + t.Fatalf("span was not exported") + } + var spans []model.SpanModel + json.Unmarshal(data, &spans) + if len(spans) != 1 { + t.Fatalf("Export: got %d spans, want 1", len(spans)) + } + got := spans[0] + got.SpanContext.Sampled = &sampledTrue // Sampled is not set when the span is reported. + if len(got.Annotations) != len(tt.want.Annotations) { + t.Fatalf("Export: got %d annotations in span, want %d", len(got.Annotations), len(tt.want.Annotations)) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Export:\n\tgot %#v\n\twant %#v", got, tt.want) + } + } +} diff --git a/vendor/go.opencensus.io/go.mod b/vendor/go.opencensus.io/go.mod new file mode 100644 index 0000000000..1236f4c2f3 --- /dev/null +++ b/vendor/go.opencensus.io/go.mod @@ -0,0 +1,25 @@ +module go.opencensus.io + +require ( + git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999 + github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 + github.com/ghodss/yaml v1.0.0 // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/protobuf v1.2.0 + github.com/google/go-cmp v0.2.0 + github.com/grpc-ecosystem/grpc-gateway v1.5.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 + github.com/openzipkin/zipkin-go v0.1.1 + github.com/prometheus/client_golang v0.8.0 + github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 + github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e + github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 + golang.org/x/net v0.0.0-20180906233101-161cd47e91fd + golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f + golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e + golang.org/x/text v0.3.0 + google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf + google.golang.org/genproto v0.0.0-20180831171423-11092d34479b + google.golang.org/grpc v1.14.0 + gopkg.in/yaml.v2 v2.2.1 // indirect +) diff --git a/vendor/go.opencensus.io/go.sum b/vendor/go.opencensus.io/go.sum new file mode 100644 index 0000000000..3e0bab8845 --- /dev/null +++ b/vendor/go.opencensus.io/go.sum @@ -0,0 +1,48 @@ +git.apache.org/thrift.git v0.0.0-20180807212849-6e67faa92827/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999 h1:sihTnRgTOUSCQz0iS0pjZuFQy/z7GXCJgSBg3+rZKHw= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/grpc-ecosystem/grpc-gateway v1.5.0 h1:WcmKMm43DR7RdtlkEXQJyo5ws8iTp98CyhCCbOHMvNI= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/openzipkin/zipkin-go v0.1.1 h1:A/ADD6HaPnAKj3yS7HjGHRK77qi41Hi0DirOOIQAeIw= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/prometheus/client_golang v0.8.0 h1:1921Yw9Gc3iSc4VQh3PIoOqgPCZS7G/4xQNVUp8Mda8= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e h1:n/3MEhJQjQxrOUCzh1Y3Re6aJUUWRp2M9+Oc3eVn/54= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 h1:agujYaXJSxSo18YNX3jzl+4G6Bstwt+kqv47GS12uL0= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +golang.org/x/net v0.0.0-20180821023952-922f4815f713/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180821140842-3b58ed4ad339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/api v0.0.0-20180818000503-e21acd801f91/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf h1:rjxqQmxjyqerRKEj+tZW+MCm4LgpFXu18bsEoCMgDsk= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.14.0 h1:ArxJuB1NWfPY6r9Gp9gqwplT0Ge7nqv9msgu03lHLmo= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/go.opencensus.io/internal/check/version.go b/vendor/go.opencensus.io/internal/check/version.go new file mode 100644 index 0000000000..ab57ae73dd --- /dev/null +++ b/vendor/go.opencensus.io/internal/check/version.go @@ -0,0 +1,88 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Command version checks that the version string matches the latest Git tag. +// This is expected to pass only on the master branch. +package main + +import ( + "bytes" + "fmt" + "log" + "os" + "os/exec" + "sort" + "strconv" + "strings" + + opencensus "go.opencensus.io" +) + +func main() { + cmd := exec.Command("git", "tag") + var buf bytes.Buffer + cmd.Stdout = &buf + err := cmd.Run() + if err != nil { + log.Fatal(err) + } + var versions []version + for _, vStr := range strings.Split(buf.String(), "\n") { + if len(vStr) == 0 { + continue + } + versions = append(versions, parseVersion(vStr)) + } + sort.Slice(versions, func(i, j int) bool { + return versionLess(versions[i], versions[j]) + }) + latest := versions[len(versions)-1] + codeVersion := parseVersion("v" + opencensus.Version()) + if !versionLess(latest, codeVersion) { + fmt.Printf("exporter.Version is out of date with Git tags. Got %s; want something greater than %s\n", opencensus.Version(), latest) + os.Exit(1) + } + fmt.Printf("exporter.Version is up-to-date: %s\n", opencensus.Version()) +} + +type version [3]int + +func versionLess(v1, v2 version) bool { + for c := 0; c < 3; c++ { + if diff := v1[c] - v2[c]; diff != 0 { + return diff < 0 + } + } + return false +} + +func parseVersion(vStr string) version { + split := strings.Split(vStr[1:], ".") + var ( + v version + err error + ) + for i := 0; i < 3; i++ { + v[i], err = strconv.Atoi(split[i]) + if err != nil { + fmt.Printf("Unrecognized version tag %q: %s\n", vStr, err) + os.Exit(2) + } + } + return v +} + +func (v version) String() string { + return fmt.Sprintf("%d.%d.%d", v[0], v[1], v[2]) +} diff --git a/vendor/go.opencensus.io/internal/internal.go b/vendor/go.opencensus.io/internal/internal.go new file mode 100644 index 0000000000..e1d1238d01 --- /dev/null +++ b/vendor/go.opencensus.io/internal/internal.go @@ -0,0 +1,37 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal // import "go.opencensus.io/internal" + +import ( + "fmt" + "time" + + "go.opencensus.io" +) + +// UserAgent is the user agent to be added to the outgoing +// requests from the exporters. +var UserAgent = fmt.Sprintf("opencensus-go [%s]", opencensus.Version()) + +// MonotonicEndTime returns the end time at present +// but offset from start, monotonically. +// +// The monotonic clock is used in subtractions hence +// the duration since start added back to start gives +// end as a monotonic time. +// See https://golang.org/pkg/time/#hdr-Monotonic_Clocks +func MonotonicEndTime(start time.Time) time.Time { + return start.Add(time.Now().Sub(start)) +} diff --git a/vendor/go.opencensus.io/internal/readme/README.md b/vendor/go.opencensus.io/internal/readme/README.md new file mode 100644 index 0000000000..c744016daf --- /dev/null +++ b/vendor/go.opencensus.io/internal/readme/README.md @@ -0,0 +1,6 @@ +Use the following commands to regenerate the README. + +```bash +$ go get github.com/rakyll/embedmd +$ embedmd -w ../../README.md +``` diff --git a/vendor/go.opencensus.io/internal/readme/mkdocs.sh b/vendor/go.opencensus.io/internal/readme/mkdocs.sh new file mode 100755 index 0000000000..4b1b0661c8 --- /dev/null +++ b/vendor/go.opencensus.io/internal/readme/mkdocs.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +embedmd source.md > ../../README.md diff --git a/vendor/go.opencensus.io/internal/readme/stats.go b/vendor/go.opencensus.io/internal/readme/stats.go new file mode 100644 index 0000000000..e8a27ff980 --- /dev/null +++ b/vendor/go.opencensus.io/internal/readme/stats.go @@ -0,0 +1,56 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package readme generates the README. +package readme // import "go.opencensus.io/internal/readme" + +import ( + "context" + "log" + + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" +) + +// README.md is generated with the examples here by using embedmd. +// For more details, see https://github.com/rakyll/embedmd. + +func statsExamples() { + ctx := context.Background() + + videoSize := stats.Int64("example.com/video_size", "processed video size", "MB") + + // START aggs + distAgg := view.Distribution(0, 1<<32, 2<<32, 3<<32) + countAgg := view.Count() + sumAgg := view.Sum() + // END aggs + + _, _, _ = distAgg, countAgg, sumAgg + + // START view + if err := view.Register(&view.View{ + Name: "example.com/video_size_distribution", + Description: "distribution of processed video size over time", + Measure: videoSize, + Aggregation: view.Distribution(0, 1<<32, 2<<32, 3<<32), + }); err != nil { + log.Fatalf("Failed to register view: %v", err) + } + // END view + + // START record + stats.Record(ctx, videoSize.M(102478)) + // END record +} diff --git a/vendor/go.opencensus.io/internal/readme/tags.go b/vendor/go.opencensus.io/internal/readme/tags.go new file mode 100644 index 0000000000..09d9ac12f4 --- /dev/null +++ b/vendor/go.opencensus.io/internal/readme/tags.go @@ -0,0 +1,60 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package readme + +import ( + "context" + "log" + + "go.opencensus.io/tag" +) + +func tagsExamples() { + ctx := context.Background() + + osKey, err := tag.NewKey("example.com/keys/user-os") + if err != nil { + log.Fatal(err) + } + userIDKey, err := tag.NewKey("example.com/keys/user-id") + if err != nil { + log.Fatal(err) + } + + // START new + ctx, err = tag.New(ctx, + tag.Insert(osKey, "macOS-10.12.5"), + tag.Upsert(userIDKey, "cde36753ed"), + ) + if err != nil { + log.Fatal(err) + } + // END new + + // START profiler + ctx, err = tag.New(ctx, + tag.Insert(osKey, "macOS-10.12.5"), + tag.Insert(userIDKey, "fff0989878"), + ) + if err != nil { + log.Fatal(err) + } + tag.Do(ctx, func(ctx context.Context) { + // Do work. + // When profiling is on, samples will be + // recorded with the key/values from the tag map. + }) + // END profiler +} diff --git a/vendor/go.opencensus.io/internal/readme/trace.go b/vendor/go.opencensus.io/internal/readme/trace.go new file mode 100644 index 0000000000..43e147ef75 --- /dev/null +++ b/vendor/go.opencensus.io/internal/readme/trace.go @@ -0,0 +1,32 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package readme + +import ( + "context" + + "go.opencensus.io/trace" +) + +func traceExamples() { + ctx := context.Background() + + // START startend + ctx, span := trace.StartSpan(ctx, "cache.Get") + defer span.End() + + // Do work to get from cache. + // END startend +} diff --git a/vendor/go.opencensus.io/internal/sanitize.go b/vendor/go.opencensus.io/internal/sanitize.go new file mode 100644 index 0000000000..de8ccf236c --- /dev/null +++ b/vendor/go.opencensus.io/internal/sanitize.go @@ -0,0 +1,50 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "strings" + "unicode" +) + +const labelKeySizeLimit = 100 + +// Sanitize returns a string that is trunacated to 100 characters if it's too +// long, and replaces non-alphanumeric characters to underscores. +func Sanitize(s string) string { + if len(s) == 0 { + return s + } + if len(s) > labelKeySizeLimit { + s = s[:labelKeySizeLimit] + } + s = strings.Map(sanitizeRune, s) + if unicode.IsDigit(rune(s[0])) { + s = "key_" + s + } + if s[0] == '_' { + s = "key" + s + } + return s +} + +// converts anything that is not a letter or digit to an underscore +func sanitizeRune(r rune) rune { + if unicode.IsLetter(r) || unicode.IsDigit(r) { + return r + } + // Everything else turns into an underscore + return '_' +} diff --git a/vendor/go.opencensus.io/internal/sanitize_test.go b/vendor/go.opencensus.io/internal/sanitize_test.go new file mode 100644 index 0000000000..6c02c7da02 --- /dev/null +++ b/vendor/go.opencensus.io/internal/sanitize_test.go @@ -0,0 +1,67 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "strings" + "testing" +) + +func TestSanitize(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "trunacate long string", + input: strings.Repeat("a", 101), + want: strings.Repeat("a", 100), + }, + { + name: "replace character", + input: "test/key-1", + want: "test_key_1", + }, + { + name: "add prefix if starting with digit", + input: "0123456789", + want: "key_0123456789", + }, + { + name: "add prefix if starting with _", + input: "_0123456789", + want: "key_0123456789", + }, + { + name: "starts with _ after sanitization", + input: "/0123456789", + want: "key_0123456789", + }, + { + name: "valid input", + input: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789", + want: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got, want := Sanitize(tt.input), tt.want; got != want { + t.Errorf("sanitize() = %q; want %q", got, want) + } + }) + } +} diff --git a/vendor/go.opencensus.io/internal/tagencoding/tagencoding.go b/vendor/go.opencensus.io/internal/tagencoding/tagencoding.go new file mode 100644 index 0000000000..3b1af8b4b8 --- /dev/null +++ b/vendor/go.opencensus.io/internal/tagencoding/tagencoding.go @@ -0,0 +1,72 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Package tagencoding contains the tag encoding +// used interally by the stats collector. +package tagencoding // import "go.opencensus.io/internal/tagencoding" + +type Values struct { + Buffer []byte + WriteIndex int + ReadIndex int +} + +func (vb *Values) growIfRequired(expected int) { + if len(vb.Buffer)-vb.WriteIndex < expected { + tmp := make([]byte, 2*(len(vb.Buffer)+1)+expected) + copy(tmp, vb.Buffer) + vb.Buffer = tmp + } +} + +func (vb *Values) WriteValue(v []byte) { + length := len(v) & 0xff + vb.growIfRequired(1 + length) + + // writing length of v + vb.Buffer[vb.WriteIndex] = byte(length) + vb.WriteIndex++ + + if length == 0 { + // No value was encoded for this key + return + } + + // writing v + copy(vb.Buffer[vb.WriteIndex:], v[:length]) + vb.WriteIndex += length +} + +// ReadValue is the helper method to read the values when decoding valuesBytes to a map[Key][]byte. +func (vb *Values) ReadValue() []byte { + // read length of v + length := int(vb.Buffer[vb.ReadIndex]) + vb.ReadIndex++ + if length == 0 { + // No value was encoded for this key + return nil + } + + // read value of v + v := make([]byte, length) + endIdx := vb.ReadIndex + length + copy(v, vb.Buffer[vb.ReadIndex:endIdx]) + vb.ReadIndex = endIdx + return v +} + +func (vb *Values) Bytes() []byte { + return vb.Buffer[:vb.WriteIndex] +} diff --git a/vendor/go.opencensus.io/internal/testpb/generate.sh b/vendor/go.opencensus.io/internal/testpb/generate.sh new file mode 100755 index 0000000000..9ee6b1e263 --- /dev/null +++ b/vendor/go.opencensus.io/internal/testpb/generate.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# generate .pb.go file from .proto file. +set -e +protoc --go_out=plugins=grpc:. test.proto +echo '//go:generate ./generate.sh +' >> test.pb.go + diff --git a/vendor/go.opencensus.io/internal/testpb/impl.go b/vendor/go.opencensus.io/internal/testpb/impl.go new file mode 100644 index 0000000000..24533afcd5 --- /dev/null +++ b/vendor/go.opencensus.io/internal/testpb/impl.go @@ -0,0 +1,93 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testpb + +import ( + "context" + "fmt" + "io" + "net" + "testing" + "time" + + "go.opencensus.io/plugin/ocgrpc" + "go.opencensus.io/trace" + "google.golang.org/grpc" +) + +type testServer struct{} + +var _ FooServer = (*testServer)(nil) + +func (s *testServer) Single(ctx context.Context, in *FooRequest) (*FooResponse, error) { + if in.SleepNanos > 0 { + _, span := trace.StartSpan(ctx, "testpb.Single.Sleep") + span.AddAttributes(trace.Int64Attribute("sleep_nanos", in.SleepNanos)) + time.Sleep(time.Duration(in.SleepNanos)) + span.End() + } + if in.Fail { + return nil, fmt.Errorf("request failed") + } + return &FooResponse{}, nil +} + +func (s *testServer) Multiple(stream Foo_MultipleServer) error { + for { + in, err := stream.Recv() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + if in.Fail { + return fmt.Errorf("request failed") + } + if err := stream.Send(&FooResponse{}); err != nil { + return err + } + } +} + +func NewTestClient(l *testing.T) (client FooClient, cleanup func()) { + // initialize server + listener, err := net.Listen("tcp", "localhost:0") + if err != nil { + l.Fatal(err) + } + server := grpc.NewServer(grpc.StatsHandler(&ocgrpc.ServerHandler{})) + RegisterFooServer(server, &testServer{}) + go server.Serve(listener) + + // Initialize client. + clientConn, err := grpc.Dial( + listener.Addr().String(), + grpc.WithInsecure(), + grpc.WithStatsHandler(&ocgrpc.ClientHandler{}), + grpc.WithBlock()) + + if err != nil { + l.Fatal(err) + } + client = NewFooClient(clientConn) + + cleanup = func() { + server.GracefulStop() + clientConn.Close() + } + + return client, cleanup +} diff --git a/vendor/go.opencensus.io/internal/testpb/test.pb.go b/vendor/go.opencensus.io/internal/testpb/test.pb.go new file mode 100644 index 0000000000..546c1d75ce --- /dev/null +++ b/vendor/go.opencensus.io/internal/testpb/test.pb.go @@ -0,0 +1,228 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: test.proto + +/* +Package testpb is a generated protocol buffer package. + +It is generated from these files: + test.proto + +It has these top-level messages: + FooRequest + FooResponse +*/ +package testpb // import "go.opencensus.io/internal/testpb" + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type FooRequest struct { + Fail bool `protobuf:"varint,1,opt,name=fail" json:"fail,omitempty"` + SleepNanos int64 `protobuf:"varint,2,opt,name=sleep_nanos,json=sleepNanos" json:"sleep_nanos,omitempty"` +} + +func (m *FooRequest) Reset() { *m = FooRequest{} } +func (m *FooRequest) String() string { return proto.CompactTextString(m) } +func (*FooRequest) ProtoMessage() {} +func (*FooRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *FooRequest) GetFail() bool { + if m != nil { + return m.Fail + } + return false +} + +func (m *FooRequest) GetSleepNanos() int64 { + if m != nil { + return m.SleepNanos + } + return 0 +} + +type FooResponse struct { +} + +func (m *FooResponse) Reset() { *m = FooResponse{} } +func (m *FooResponse) String() string { return proto.CompactTextString(m) } +func (*FooResponse) ProtoMessage() {} +func (*FooResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } + +func init() { + proto.RegisterType((*FooRequest)(nil), "testpb.FooRequest") + proto.RegisterType((*FooResponse)(nil), "testpb.FooResponse") +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// Client API for Foo service + +type FooClient interface { + Single(ctx context.Context, in *FooRequest, opts ...grpc.CallOption) (*FooResponse, error) + Multiple(ctx context.Context, opts ...grpc.CallOption) (Foo_MultipleClient, error) +} + +type fooClient struct { + cc *grpc.ClientConn +} + +func NewFooClient(cc *grpc.ClientConn) FooClient { + return &fooClient{cc} +} + +func (c *fooClient) Single(ctx context.Context, in *FooRequest, opts ...grpc.CallOption) (*FooResponse, error) { + out := new(FooResponse) + err := grpc.Invoke(ctx, "/testpb.Foo/Single", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *fooClient) Multiple(ctx context.Context, opts ...grpc.CallOption) (Foo_MultipleClient, error) { + stream, err := grpc.NewClientStream(ctx, &_Foo_serviceDesc.Streams[0], c.cc, "/testpb.Foo/Multiple", opts...) + if err != nil { + return nil, err + } + x := &fooMultipleClient{stream} + return x, nil +} + +type Foo_MultipleClient interface { + Send(*FooRequest) error + Recv() (*FooResponse, error) + grpc.ClientStream +} + +type fooMultipleClient struct { + grpc.ClientStream +} + +func (x *fooMultipleClient) Send(m *FooRequest) error { + return x.ClientStream.SendMsg(m) +} + +func (x *fooMultipleClient) Recv() (*FooResponse, error) { + m := new(FooResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// Server API for Foo service + +type FooServer interface { + Single(context.Context, *FooRequest) (*FooResponse, error) + Multiple(Foo_MultipleServer) error +} + +func RegisterFooServer(s *grpc.Server, srv FooServer) { + s.RegisterService(&_Foo_serviceDesc, srv) +} + +func _Foo_Single_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FooRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(FooServer).Single(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/testpb.Foo/Single", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(FooServer).Single(ctx, req.(*FooRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Foo_Multiple_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(FooServer).Multiple(&fooMultipleServer{stream}) +} + +type Foo_MultipleServer interface { + Send(*FooResponse) error + Recv() (*FooRequest, error) + grpc.ServerStream +} + +type fooMultipleServer struct { + grpc.ServerStream +} + +func (x *fooMultipleServer) Send(m *FooResponse) error { + return x.ServerStream.SendMsg(m) +} + +func (x *fooMultipleServer) Recv() (*FooRequest, error) { + m := new(FooRequest) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +var _Foo_serviceDesc = grpc.ServiceDesc{ + ServiceName: "testpb.Foo", + HandlerType: (*FooServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Single", + Handler: _Foo_Single_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "Multiple", + Handler: _Foo_Multiple_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "test.proto", +} + +func init() { proto.RegisterFile("test.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 165 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2a, 0x49, 0x2d, 0x2e, + 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x03, 0xb1, 0x0b, 0x92, 0x94, 0x1c, 0xb9, 0xb8, + 0xdc, 0xf2, 0xf3, 0x83, 0x52, 0x0b, 0x4b, 0x53, 0x8b, 0x4b, 0x84, 0x84, 0xb8, 0x58, 0xd2, 0x12, + 0x33, 0x73, 0x24, 0x18, 0x15, 0x18, 0x35, 0x38, 0x82, 0xc0, 0x6c, 0x21, 0x79, 0x2e, 0xee, 0xe2, + 0x9c, 0xd4, 0xd4, 0x82, 0xf8, 0xbc, 0xc4, 0xbc, 0xfc, 0x62, 0x09, 0x26, 0x05, 0x46, 0x0d, 0xe6, + 0x20, 0x2e, 0xb0, 0x90, 0x1f, 0x48, 0x44, 0x89, 0x97, 0x8b, 0x1b, 0x6c, 0x44, 0x71, 0x41, 0x7e, + 0x5e, 0x71, 0xaa, 0x51, 0x21, 0x17, 0xb3, 0x5b, 0x7e, 0xbe, 0x90, 0x21, 0x17, 0x5b, 0x70, 0x66, + 0x5e, 0x7a, 0x4e, 0xaa, 0x90, 0x90, 0x1e, 0xc4, 0x2e, 0x3d, 0x84, 0x45, 0x52, 0xc2, 0x28, 0x62, + 0x10, 0x9d, 0x42, 0xe6, 0x5c, 0x1c, 0xbe, 0xa5, 0x39, 0x25, 0x99, 0x05, 0x24, 0x68, 0xd2, 0x60, + 0x34, 0x60, 0x4c, 0x62, 0x03, 0xfb, 0xc9, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x37, 0xb1, 0x2d, + 0x6e, 0xe1, 0x00, 0x00, 0x00, +} + +//go:generate ./generate.sh diff --git a/vendor/go.opencensus.io/internal/testpb/test.proto b/vendor/go.opencensus.io/internal/testpb/test.proto new file mode 100644 index 0000000000..b82d128acf --- /dev/null +++ b/vendor/go.opencensus.io/internal/testpb/test.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package testpb; + +message FooRequest { + bool fail = 1; + int64 sleep_nanos = 2; +} + +message FooResponse { +} + +service Foo { + rpc Single(FooRequest) returns (FooResponse); + rpc Multiple(stream FooRequest) returns (stream FooResponse); +} diff --git a/vendor/go.opencensus.io/internal/traceinternals.go b/vendor/go.opencensus.io/internal/traceinternals.go new file mode 100644 index 0000000000..553ca68dc4 --- /dev/null +++ b/vendor/go.opencensus.io/internal/traceinternals.go @@ -0,0 +1,52 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "time" +) + +// Trace allows internal access to some trace functionality. +// TODO(#412): remove this +var Trace interface{} + +var LocalSpanStoreEnabled bool + +// BucketConfiguration stores the number of samples to store for span buckets +// for successful and failed spans for a particular span name. +type BucketConfiguration struct { + Name string + MaxRequestsSucceeded int + MaxRequestsErrors int +} + +// PerMethodSummary is a summary of the spans stored for a single span name. +type PerMethodSummary struct { + Active int + LatencyBuckets []LatencyBucketSummary + ErrorBuckets []ErrorBucketSummary +} + +// LatencyBucketSummary is a summary of a latency bucket. +type LatencyBucketSummary struct { + MinLatency, MaxLatency time.Duration + Size int +} + +// ErrorBucketSummary is a summary of an error bucket. +type ErrorBucketSummary struct { + ErrorCode int32 + Size int +} diff --git a/vendor/go.opencensus.io/opencensus.go b/vendor/go.opencensus.io/opencensus.go new file mode 100644 index 0000000000..62f03486a2 --- /dev/null +++ b/vendor/go.opencensus.io/opencensus.go @@ -0,0 +1,21 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package opencensus contains Go support for OpenCensus. +package opencensus // import "go.opencensus.io" + +// Version is the current release version of OpenCensus in use. +func Version() string { + return "0.18.0" +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/benchmark_test.go b/vendor/go.opencensus.io/plugin/ocgrpc/benchmark_test.go new file mode 100644 index 0000000000..7c80776ecf --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/benchmark_test.go @@ -0,0 +1,65 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ocgrpc + +import ( + "testing" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func BenchmarkStatusCodeToString_OK(b *testing.B) { + st := status.New(codes.OK, "OK") + for i := 0; i < b.N; i++ { + s := statusCodeToString(st) + _ = s + } +} + +func BenchmarkStatusCodeToString_Unauthenticated(b *testing.B) { + st := status.New(codes.Unauthenticated, "Unauthenticated") + for i := 0; i < b.N; i++ { + s := statusCodeToString(st) + _ = s + } +} + +var codeToStringMap = map[codes.Code]string{ + codes.OK: "OK", + codes.Canceled: "CANCELLED", + codes.Unknown: "UNKNOWN", + codes.InvalidArgument: "INVALID_ARGUMENT", + codes.DeadlineExceeded: "DEADLINE_EXCEEDED", + codes.NotFound: "NOT_FOUND", + codes.AlreadyExists: "ALREADY_EXISTS", + codes.PermissionDenied: "PERMISSION_DENIED", + codes.ResourceExhausted: "RESOURCE_EXHAUSTED", + codes.FailedPrecondition: "FAILED_PRECONDITION", + codes.Aborted: "ABORTED", + codes.OutOfRange: "OUT_OF_RANGE", + codes.Unimplemented: "UNIMPLEMENTED", + codes.Internal: "INTERNAL", + codes.Unavailable: "UNAVAILABLE", + codes.DataLoss: "DATA_LOSS", + codes.Unauthenticated: "UNAUTHENTICATED", +} + +func BenchmarkMapAlternativeImpl_OK(b *testing.B) { + st := status.New(codes.OK, "OK") + for i := 0; i < b.N; i++ { + _ = codeToStringMap[st.Code()] + } +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/client.go b/vendor/go.opencensus.io/plugin/ocgrpc/client.go new file mode 100644 index 0000000000..a6c466ae82 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/client.go @@ -0,0 +1,56 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ocgrpc + +import ( + "go.opencensus.io/trace" + "golang.org/x/net/context" + + "google.golang.org/grpc/stats" +) + +// ClientHandler implements a gRPC stats.Handler for recording OpenCensus stats and +// traces. Use with gRPC clients only. +type ClientHandler struct { + // StartOptions allows configuring the StartOptions used to create new spans. + // + // StartOptions.SpanKind will always be set to trace.SpanKindClient + // for spans started by this handler. + StartOptions trace.StartOptions +} + +// HandleConn exists to satisfy gRPC stats.Handler. +func (c *ClientHandler) HandleConn(ctx context.Context, cs stats.ConnStats) { + // no-op +} + +// TagConn exists to satisfy gRPC stats.Handler. +func (c *ClientHandler) TagConn(ctx context.Context, cti *stats.ConnTagInfo) context.Context { + // no-op + return ctx +} + +// HandleRPC implements per-RPC tracing and stats instrumentation. +func (c *ClientHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) { + traceHandleRPC(ctx, rs) + statsHandleRPC(ctx, rs) +} + +// TagRPC implements per-RPC context management. +func (c *ClientHandler) TagRPC(ctx context.Context, rti *stats.RPCTagInfo) context.Context { + ctx = c.traceTagRPC(ctx, rti) + ctx = c.statsTagRPC(ctx, rti) + return ctx +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/client_metrics.go b/vendor/go.opencensus.io/plugin/ocgrpc/client_metrics.go new file mode 100644 index 0000000000..abe978b67b --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/client_metrics.go @@ -0,0 +1,107 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package ocgrpc + +import ( + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +// The following variables are measures are recorded by ClientHandler: +var ( + ClientSentMessagesPerRPC = stats.Int64("grpc.io/client/sent_messages_per_rpc", "Number of messages sent in the RPC (always 1 for non-streaming RPCs).", stats.UnitDimensionless) + ClientSentBytesPerRPC = stats.Int64("grpc.io/client/sent_bytes_per_rpc", "Total bytes sent across all request messages per RPC.", stats.UnitBytes) + ClientReceivedMessagesPerRPC = stats.Int64("grpc.io/client/received_messages_per_rpc", "Number of response messages received per RPC (always 1 for non-streaming RPCs).", stats.UnitDimensionless) + ClientReceivedBytesPerRPC = stats.Int64("grpc.io/client/received_bytes_per_rpc", "Total bytes received across all response messages per RPC.", stats.UnitBytes) + ClientRoundtripLatency = stats.Float64("grpc.io/client/roundtrip_latency", "Time between first byte of request sent to last byte of response received, or terminal error.", stats.UnitMilliseconds) + ClientServerLatency = stats.Float64("grpc.io/client/server_latency", `Propagated from the server and should have the same value as "grpc.io/server/latency".`, stats.UnitMilliseconds) +) + +// Predefined views may be registered to collect data for the above measures. +// As always, you may also define your own custom views over measures collected by this +// package. These are declared as a convenience only; none are registered by +// default. +var ( + ClientSentBytesPerRPCView = &view.View{ + Measure: ClientSentBytesPerRPC, + Name: "grpc.io/client/sent_bytes_per_rpc", + Description: "Distribution of bytes sent per RPC, by method.", + TagKeys: []tag.Key{KeyClientMethod}, + Aggregation: DefaultBytesDistribution, + } + + ClientReceivedBytesPerRPCView = &view.View{ + Measure: ClientReceivedBytesPerRPC, + Name: "grpc.io/client/received_bytes_per_rpc", + Description: "Distribution of bytes received per RPC, by method.", + TagKeys: []tag.Key{KeyClientMethod}, + Aggregation: DefaultBytesDistribution, + } + + ClientRoundtripLatencyView = &view.View{ + Measure: ClientRoundtripLatency, + Name: "grpc.io/client/roundtrip_latency", + Description: "Distribution of round-trip latency, by method.", + TagKeys: []tag.Key{KeyClientMethod}, + Aggregation: DefaultMillisecondsDistribution, + } + + ClientCompletedRPCsView = &view.View{ + Measure: ClientRoundtripLatency, + Name: "grpc.io/client/completed_rpcs", + Description: "Count of RPCs by method and status.", + TagKeys: []tag.Key{KeyClientMethod, KeyClientStatus}, + Aggregation: view.Count(), + } + + ClientSentMessagesPerRPCView = &view.View{ + Measure: ClientSentMessagesPerRPC, + Name: "grpc.io/client/sent_messages_per_rpc", + Description: "Distribution of sent messages count per RPC, by method.", + TagKeys: []tag.Key{KeyClientMethod}, + Aggregation: DefaultMessageCountDistribution, + } + + ClientReceivedMessagesPerRPCView = &view.View{ + Measure: ClientReceivedMessagesPerRPC, + Name: "grpc.io/client/received_messages_per_rpc", + Description: "Distribution of received messages count per RPC, by method.", + TagKeys: []tag.Key{KeyClientMethod}, + Aggregation: DefaultMessageCountDistribution, + } + + ClientServerLatencyView = &view.View{ + Measure: ClientServerLatency, + Name: "grpc.io/client/server_latency", + Description: "Distribution of server latency as viewed by client, by method.", + TagKeys: []tag.Key{KeyClientMethod}, + Aggregation: DefaultMillisecondsDistribution, + } +) + +// DefaultClientViews are the default client views provided by this package. +var DefaultClientViews = []*view.View{ + ClientSentBytesPerRPCView, + ClientReceivedBytesPerRPCView, + ClientRoundtripLatencyView, + ClientCompletedRPCsView, +} + +// TODO(jbd): Add roundtrip_latency, uncompressed_request_bytes, uncompressed_response_bytes, request_count, response_count. +// TODO(acetechnologist): This is temporary and will need to be replaced by a +// mechanism to load these defaults from a common repository/config shared by +// all supported languages. Likely a serialized protobuf of these defaults. diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/client_spec_test.go b/vendor/go.opencensus.io/plugin/ocgrpc/client_spec_test.go new file mode 100644 index 0000000000..860d9f3426 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/client_spec_test.go @@ -0,0 +1,152 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package ocgrpc + +import ( + "regexp" + "strings" + "testing" + + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" +) + +var colSep = regexp.MustCompile(`\s*\|\s*`) + +func TestSpecClientMeasures(t *testing.T) { + spec := ` +| Measure name | Unit | Description | +|------------------------------------------|------|-----------------------------------------------------------------------------------------------| +| grpc.io/client/sent_messages_per_rpc | 1 | Number of messages sent in the RPC (always 1 for non-streaming RPCs). | +| grpc.io/client/sent_bytes_per_rpc | By | Total bytes sent across all request messages per RPC. | +| grpc.io/client/received_messages_per_rpc | 1 | Number of response messages received per RPC (always 1 for non-streaming RPCs). | +| grpc.io/client/received_bytes_per_rpc | By | Total bytes received across all response messages per RPC. | +| grpc.io/client/roundtrip_latency | ms | Time between first byte of request sent to last byte of response received, or terminal error. | +| grpc.io/client/server_latency | ms | Propagated from the server and should have the same value as "grpc.io/server/latency". |` + + lines := strings.Split(spec, "\n")[3:] + type measureDef struct { + name string + unit string + desc string + } + measureDefs := make([]measureDef, 0, len(lines)) + for _, line := range lines { + cols := colSep.Split(line, -1)[1:] + if len(cols) < 3 { + t.Fatalf("Invalid config line %#v", cols) + } + measureDefs = append(measureDefs, measureDef{cols[0], cols[1], cols[2]}) + } + + gotMeasures := []stats.Measure{ + ClientSentMessagesPerRPC, + ClientSentBytesPerRPC, + ClientReceivedMessagesPerRPC, + ClientReceivedBytesPerRPC, + ClientRoundtripLatency, + ClientServerLatency, + } + + if got, want := len(gotMeasures), len(measureDefs); got != want { + t.Fatalf("len(gotMeasures) = %d; want %d", got, want) + } + + for i, m := range gotMeasures { + defn := measureDefs[i] + if got, want := m.Name(), defn.name; got != want { + t.Errorf("Name = %q; want %q", got, want) + } + if got, want := m.Unit(), defn.unit; got != want { + t.Errorf("%q: Unit = %q; want %q", defn.name, got, want) + } + if got, want := m.Description(), defn.desc; got != want { + t.Errorf("%q: Description = %q; want %q", defn.name, got, want) + } + } +} + +func TestSpecClientViews(t *testing.T) { + defaultViewsSpec := ` +| View name | Measure suffix | Aggregation | Tags | +|---------------------------------------|------------------------|--------------|------------------------------| +| grpc.io/client/sent_bytes_per_rpc | sent_bytes_per_rpc | distribution | client_method | +| grpc.io/client/received_bytes_per_rpc | received_bytes_per_rpc | distribution | client_method | +| grpc.io/client/roundtrip_latency | roundtrip_latency | distribution | client_method | +| grpc.io/client/completed_rpcs | roundtrip_latency | count | client_method, client_status |` + + extraViewsSpec := ` +| View name | Measure suffix | Aggregation | Tags suffix | +|------------------------------------------|---------------------------|--------------|---------------| +| grpc.io/client/sent_messages_per_rpc | sent_messages_per_rpc | distribution | client_method | +| grpc.io/client/received_messages_per_rpc | received_messages_per_rpc | distribution | client_method | +| grpc.io/client/server_latency | server_latency | distribution | client_method |` + + lines := strings.Split(defaultViewsSpec, "\n")[3:] + lines = append(lines, strings.Split(extraViewsSpec, "\n")[3:]...) + type viewDef struct { + name string + measureSuffix string + aggregation string + tags string + } + viewDefs := make([]viewDef, 0, len(lines)) + for _, line := range lines { + cols := colSep.Split(line, -1)[1:] + if len(cols) < 4 { + t.Fatalf("Invalid config line %#v", cols) + } + viewDefs = append(viewDefs, viewDef{cols[0], cols[1], cols[2], cols[3]}) + } + + views := DefaultClientViews + views = append(views, ClientSentMessagesPerRPCView, ClientReceivedMessagesPerRPCView, ClientServerLatencyView) + + if got, want := len(views), len(viewDefs); got != want { + t.Fatalf("len(gotMeasures) = %d; want %d", got, want) + } + + for i, v := range views { + defn := viewDefs[i] + if got, want := v.Name, defn.name; got != want { + t.Errorf("Name = %q; want %q", got, want) + } + if got, want := v.Measure.Name(), "grpc.io/client/"+defn.measureSuffix; got != want { + t.Errorf("%q: Measure.Name = %q; want %q", defn.name, got, want) + } + switch v.Aggregation.Type { + case view.AggTypeDistribution: + if got, want := "distribution", defn.aggregation; got != want { + t.Errorf("%q: Description = %q; want %q", defn.name, got, want) + } + case view.AggTypeCount: + if got, want := "count", defn.aggregation; got != want { + t.Errorf("%q: Description = %q; want %q", defn.name, got, want) + } + default: + t.Errorf("Invalid aggregation type") + } + wantTags := strings.Split(defn.tags, ", ") + if got, want := len(v.TagKeys), len(wantTags); got != want { + t.Errorf("len(TagKeys) = %d; want %d", got, want) + } + for j := range wantTags { + if got, want := v.TagKeys[j].Name(), "grpc_"+wantTags[j]; got != want { + t.Errorf("TagKeys[%d].Name() = %q; want %q", j, got, want) + } + } + } +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/client_stats_handler.go b/vendor/go.opencensus.io/plugin/ocgrpc/client_stats_handler.go new file mode 100644 index 0000000000..303c607f63 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/client_stats_handler.go @@ -0,0 +1,49 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package ocgrpc + +import ( + "time" + + "go.opencensus.io/tag" + "golang.org/x/net/context" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/stats" +) + +// statsTagRPC gets the tag.Map populated by the application code, serializes +// its tags into the GRPC metadata in order to be sent to the server. +func (h *ClientHandler) statsTagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context { + startTime := time.Now() + if info == nil { + if grpclog.V(2) { + grpclog.Infof("clientHandler.TagRPC called with nil info.", info.FullMethodName) + } + return ctx + } + + d := &rpcData{ + startTime: startTime, + method: info.FullMethodName, + } + ts := tag.FromContext(ctx) + if ts != nil { + encoded := tag.Encode(ts) + ctx = stats.SetTags(ctx, encoded) + } + + return context.WithValue(ctx, rpcDataKey, d) +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/client_stats_handler_test.go b/vendor/go.opencensus.io/plugin/ocgrpc/client_stats_handler_test.go new file mode 100644 index 0000000000..990075cdbe --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/client_stats_handler_test.go @@ -0,0 +1,345 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package ocgrpc + +import ( + "testing" + + "go.opencensus.io/trace" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "golang.org/x/net/context" + + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" + + "google.golang.org/grpc/stats" +) + +func TestClientDefaultCollections(t *testing.T) { + k1, _ := tag.NewKey("k1") + k2, _ := tag.NewKey("k2") + + type tagPair struct { + k tag.Key + v string + } + + type wantData struct { + v func() *view.View + rows []*view.Row + } + type rpc struct { + tags []tagPair + tagInfo *stats.RPCTagInfo + inPayloads []*stats.InPayload + outPayloads []*stats.OutPayload + end *stats.End + } + + type testCase struct { + label string + rpcs []*rpc + wants []*wantData + } + tcs := []testCase{ + { + label: "1", + rpcs: []*rpc{ + { + []tagPair{{k1, "v1"}}, + &stats.RPCTagInfo{FullMethodName: "/package.service/method"}, + []*stats.InPayload{ + {Length: 10}, + }, + []*stats.OutPayload{ + {Length: 10}, + }, + &stats.End{Error: nil}, + }, + }, + wants: []*wantData{ + { + func() *view.View { return ClientSentMessagesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyClientMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 1, 1, 1, 0), + }, + }, + }, + { + func() *view.View { return ClientReceivedMessagesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyClientMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 1, 1, 1, 0), + }, + }, + }, + { + func() *view.View { return ClientSentBytesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyClientMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 10, 10, 10, 0), + }, + }, + }, + { + func() *view.View { return ClientReceivedBytesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyClientMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 10, 10, 10, 0), + }, + }, + }, + }, + }, + { + label: "2", + rpcs: []*rpc{ + { + []tagPair{{k1, "v1"}}, + &stats.RPCTagInfo{FullMethodName: "/package.service/method"}, + []*stats.InPayload{ + {Length: 10}, + }, + []*stats.OutPayload{ + {Length: 10}, + {Length: 10}, + {Length: 10}, + }, + &stats.End{Error: nil}, + }, + { + []tagPair{{k1, "v11"}}, + &stats.RPCTagInfo{FullMethodName: "/package.service/method"}, + []*stats.InPayload{ + {Length: 10}, + {Length: 10}, + }, + []*stats.OutPayload{ + {Length: 10}, + {Length: 10}, + }, + &stats.End{Error: status.Error(codes.Canceled, "canceled")}, + }, + }, + wants: []*wantData{ + { + func() *view.View { return ClientSentMessagesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyClientMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 2, 2, 3, 2.5, 0.5), + }, + }, + }, + { + func() *view.View { return ClientReceivedMessagesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyClientMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 2, 1, 2, 1.5, 0.5), + }, + }, + }, + }, + }, + { + label: "3", + rpcs: []*rpc{ + { + []tagPair{{k1, "v1"}}, + &stats.RPCTagInfo{FullMethodName: "/package.service/method"}, + []*stats.InPayload{ + {Length: 1}, + }, + []*stats.OutPayload{ + {Length: 1}, + {Length: 1024}, + {Length: 65536}, + }, + &stats.End{Error: nil}, + }, + { + []tagPair{{k1, "v1"}, {k2, "v2"}}, + &stats.RPCTagInfo{FullMethodName: "/package.service/method"}, + []*stats.InPayload{ + {Length: 1024}, + }, + []*stats.OutPayload{ + {Length: 4096}, + {Length: 16384}, + }, + &stats.End{Error: status.Error(codes.Canceled, "canceled")}, + }, + { + []tagPair{{k1, "v11"}, {k2, "v22"}}, + &stats.RPCTagInfo{FullMethodName: "/package.service/method"}, + []*stats.InPayload{ + {Length: 2048}, + {Length: 16384}, + }, + []*stats.OutPayload{ + {Length: 2048}, + {Length: 4096}, + {Length: 16384}, + }, + &stats.End{Error: status.Error(codes.Aborted, "aborted")}, + }, + }, + wants: []*wantData{ + { + func() *view.View { return ClientSentMessagesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyClientMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 3, 2, 3, 2.666666666, 0.333333333*2), + }, + }, + }, + { + func() *view.View { return ClientReceivedMessagesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyClientMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 3, 1, 2, 1.333333333, 0.333333333*2), + }, + }, + }, + { + func() *view.View { return ClientSentBytesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyClientMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 0, 0, 0, 2 /*16384*/, 1 /*65536*/, 0, 0, 0, 0, 0, 0, 0, 0}, 3, 20480, 66561, 36523, 1.355519318e+09), + }, + }, + }, + { + func() *view.View { return ClientReceivedBytesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyClientMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 3, 1, 18432, 6485.666667, 2.1459558466666666e+08), + }, + }, + }, + }, + }, + } + + views := []*view.View{ + ClientSentBytesPerRPCView, + ClientReceivedBytesPerRPCView, + ClientRoundtripLatencyView, + ClientCompletedRPCsView, + ClientSentMessagesPerRPCView, + ClientReceivedMessagesPerRPCView, + } + + for _, tc := range tcs { + // Register views. + if err := view.Register(views...); err != nil { + t.Error(err) + } + + h := &ClientHandler{} + h.StartOptions.Sampler = trace.NeverSample() + for _, rpc := range tc.rpcs { + var mods []tag.Mutator + for _, t := range rpc.tags { + mods = append(mods, tag.Upsert(t.k, t.v)) + } + ctx, err := tag.New(context.Background(), mods...) + if err != nil { + t.Errorf("%q: NewMap = %v", tc.label, err) + } + encoded := tag.Encode(tag.FromContext(ctx)) + ctx = stats.SetTags(context.Background(), encoded) + ctx = h.TagRPC(ctx, rpc.tagInfo) + for _, out := range rpc.outPayloads { + out.Client = true + h.HandleRPC(ctx, out) + } + for _, in := range rpc.inPayloads { + in.Client = true + h.HandleRPC(ctx, in) + } + rpc.end.Client = true + h.HandleRPC(ctx, rpc.end) + } + + for _, wantData := range tc.wants { + gotRows, err := view.RetrieveData(wantData.v().Name) + if err != nil { + t.Errorf("%q: RetrieveData(%q) = %v", tc.label, wantData.v().Name, err) + continue + } + + for _, gotRow := range gotRows { + if !containsRow(wantData.rows, gotRow) { + t.Errorf("%q: unwanted row for view %q = %v", tc.label, wantData.v().Name, gotRow) + break + } + } + + for _, wantRow := range wantData.rows { + if !containsRow(gotRows, wantRow) { + t.Errorf("%q: row missing for view %q; want %v", tc.label, wantData.v().Name, wantRow) + break + } + } + } + + // Unregister views to cleanup. + view.Unregister(views...) + } +} + +// containsRow returns true if rows contain r. +func containsRow(rows []*view.Row, r *view.Row) bool { + for _, x := range rows { + if r.Equal(x) { + return true + } + } + return false +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/doc.go b/vendor/go.opencensus.io/plugin/ocgrpc/doc.go new file mode 100644 index 0000000000..1370323fb7 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/doc.go @@ -0,0 +1,19 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package ocgrpc contains OpenCensus stats and trace +// integrations for gRPC. +// +// Use ServerHandler for servers and ClientHandler for clients. +package ocgrpc // import "go.opencensus.io/plugin/ocgrpc" diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/end_to_end_test.go b/vendor/go.opencensus.io/plugin/ocgrpc/end_to_end_test.go new file mode 100644 index 0000000000..a305bbcfff --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/end_to_end_test.go @@ -0,0 +1,239 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package ocgrpc_test + +import ( + "context" + "io" + "reflect" + "testing" + + "go.opencensus.io/internal/testpb" + "go.opencensus.io/plugin/ocgrpc" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +var keyAccountId, _ = tag.NewKey("account_id") + +func TestEndToEnd_Single(t *testing.T) { + view.Register(ocgrpc.DefaultClientViews...) + defer view.Unregister(ocgrpc.DefaultClientViews...) + view.Register(ocgrpc.DefaultServerViews...) + defer view.Unregister(ocgrpc.DefaultServerViews...) + + extraViews := []*view.View{ + ocgrpc.ServerReceivedMessagesPerRPCView, + ocgrpc.ClientReceivedMessagesPerRPCView, + ocgrpc.ServerSentMessagesPerRPCView, + ocgrpc.ClientSentMessagesPerRPCView, + } + view.Register(extraViews...) + defer view.Unregister(extraViews...) + + client, done := testpb.NewTestClient(t) + defer done() + + ctx := context.Background() + ctx, _ = tag.New(ctx, tag.Insert(keyAccountId, "abc123")) + + var ( + clientMethodTag = tag.Tag{Key: ocgrpc.KeyClientMethod, Value: "testpb.Foo/Single"} + serverMethodTag = tag.Tag{Key: ocgrpc.KeyServerMethod, Value: "testpb.Foo/Single"} + clientStatusOKTag = tag.Tag{Key: ocgrpc.KeyClientStatus, Value: "OK"} + serverStatusOKTag = tag.Tag{Key: ocgrpc.KeyServerStatus, Value: "OK"} + serverStatusUnknownTag = tag.Tag{Key: ocgrpc.KeyClientStatus, Value: "UNKNOWN"} + clientStatusUnknownTag = tag.Tag{Key: ocgrpc.KeyServerStatus, Value: "UNKNOWN"} + ) + + _, err := client.Single(ctx, &testpb.FooRequest{}) + if err != nil { + t.Fatal(err) + } + checkCount(t, ocgrpc.ClientCompletedRPCsView, 1, clientMethodTag, clientStatusOKTag) + checkCount(t, ocgrpc.ServerCompletedRPCsView, 1, serverMethodTag, serverStatusOKTag) + + _, _ = client.Single(ctx, &testpb.FooRequest{Fail: true}) + checkCount(t, ocgrpc.ClientCompletedRPCsView, 1, clientMethodTag, serverStatusUnknownTag) + checkCount(t, ocgrpc.ServerCompletedRPCsView, 1, serverMethodTag, clientStatusUnknownTag) + + tcs := []struct { + v *view.View + tags []tag.Tag + mean float64 + }{ + {ocgrpc.ClientSentMessagesPerRPCView, []tag.Tag{clientMethodTag}, 1.0}, + {ocgrpc.ServerReceivedMessagesPerRPCView, []tag.Tag{serverMethodTag}, 1.0}, + {ocgrpc.ClientReceivedMessagesPerRPCView, []tag.Tag{clientMethodTag}, 0.5}, + {ocgrpc.ServerSentMessagesPerRPCView, []tag.Tag{serverMethodTag}, 0.5}, + {ocgrpc.ClientSentBytesPerRPCView, []tag.Tag{clientMethodTag}, 1.0}, + {ocgrpc.ServerReceivedBytesPerRPCView, []tag.Tag{serverMethodTag}, 1.0}, + {ocgrpc.ClientReceivedBytesPerRPCView, []tag.Tag{clientMethodTag}, 0.0}, + {ocgrpc.ServerSentBytesPerRPCView, []tag.Tag{serverMethodTag}, 0.0}, + } + + for _, tt := range tcs { + t.Run("view="+tt.v.Name, func(t *testing.T) { + dist := getDistribution(t, tt.v, tt.tags...) + if got, want := dist.Count, int64(2); got != want { + t.Errorf("Count = %d; want %d", got, want) + } + if got, want := dist.Mean, tt.mean; got != want { + t.Errorf("Mean = %v; want %v", got, want) + } + }) + } +} + +func TestEndToEnd_Stream(t *testing.T) { + view.Register(ocgrpc.DefaultClientViews...) + defer view.Unregister(ocgrpc.DefaultClientViews...) + view.Register(ocgrpc.DefaultServerViews...) + defer view.Unregister(ocgrpc.DefaultServerViews...) + + extraViews := []*view.View{ + ocgrpc.ServerReceivedMessagesPerRPCView, + ocgrpc.ClientReceivedMessagesPerRPCView, + ocgrpc.ServerSentMessagesPerRPCView, + ocgrpc.ClientSentMessagesPerRPCView, + } + view.Register(extraViews...) + defer view.Unregister(extraViews...) + + client, done := testpb.NewTestClient(t) + defer done() + + ctx := context.Background() + ctx, _ = tag.New(ctx, tag.Insert(keyAccountId, "abc123")) + + var ( + clientMethodTag = tag.Tag{Key: ocgrpc.KeyClientMethod, Value: "testpb.Foo/Multiple"} + serverMethodTag = tag.Tag{Key: ocgrpc.KeyServerMethod, Value: "testpb.Foo/Multiple"} + clientStatusOKTag = tag.Tag{Key: ocgrpc.KeyClientStatus, Value: "OK"} + serverStatusOKTag = tag.Tag{Key: ocgrpc.KeyServerStatus, Value: "OK"} + ) + + const msgCount = 3 + + stream, err := client.Multiple(ctx) + if err != nil { + t.Fatal(err) + } + for i := 0; i < msgCount; i++ { + stream.Send(&testpb.FooRequest{}) + _, err := stream.Recv() + if err != nil { + t.Fatal(err) + } + } + if err := stream.CloseSend(); err != nil { + t.Fatal(err) + } + if _, err = stream.Recv(); err != io.EOF { + t.Fatal(err) + } + + checkCount(t, ocgrpc.ClientCompletedRPCsView, 1, clientMethodTag, clientStatusOKTag) + checkCount(t, ocgrpc.ServerCompletedRPCsView, 1, serverMethodTag, serverStatusOKTag) + + tcs := []struct { + v *view.View + tag tag.Tag + }{ + {ocgrpc.ClientSentMessagesPerRPCView, clientMethodTag}, + {ocgrpc.ServerReceivedMessagesPerRPCView, serverMethodTag}, + {ocgrpc.ServerSentMessagesPerRPCView, serverMethodTag}, + {ocgrpc.ClientReceivedMessagesPerRPCView, clientMethodTag}, + } + for _, tt := range tcs { + serverSent := getDistribution(t, tt.v, tt.tag) + if got, want := serverSent.Mean, float64(msgCount); got != want { + t.Errorf("%q.Count = %v; want %v", ocgrpc.ServerSentMessagesPerRPCView.Name, got, want) + } + } +} + +func checkCount(t *testing.T, v *view.View, want int64, tags ...tag.Tag) { + if got, ok := getCount(t, v, tags...); ok && got != want { + t.Errorf("View[name=%q].Row[tags=%v].Data = %d; want %d", v.Name, tags, got, want) + } +} + +func getCount(t *testing.T, v *view.View, tags ...tag.Tag) (int64, bool) { + if len(tags) != len(v.TagKeys) { + t.Errorf("Invalid tag specification, want %#v tags got %#v", v.TagKeys, tags) + return 0, false + } + for i := range v.TagKeys { + if tags[i].Key != v.TagKeys[i] { + t.Errorf("Invalid tag specification, want %#v tags got %#v", v.TagKeys, tags) + return 0, false + } + } + rows, err := view.RetrieveData(v.Name) + if err != nil { + t.Fatal(err) + } + var foundRow *view.Row + for _, row := range rows { + if reflect.DeepEqual(row.Tags, tags) { + foundRow = row + break + } + } + if foundRow == nil { + var gotTags [][]tag.Tag + for _, row := range rows { + gotTags = append(gotTags, row.Tags) + } + t.Errorf("Failed to find row with keys %v among:\n%v", tags, gotTags) + return 0, false + } + return foundRow.Data.(*view.CountData).Value, true +} + +func getDistribution(t *testing.T, v *view.View, tags ...tag.Tag) *view.DistributionData { + if len(tags) != len(v.TagKeys) { + t.Fatalf("Invalid tag specification, want %#v tags got %#v", v.TagKeys, tags) + return nil + } + for i := range v.TagKeys { + if tags[i].Key != v.TagKeys[i] { + t.Fatalf("Invalid tag specification, want %#v tags got %#v", v.TagKeys, tags) + return nil + } + } + rows, err := view.RetrieveData(v.Name) + if err != nil { + t.Fatal(err) + } + var foundRow *view.Row + for _, row := range rows { + if reflect.DeepEqual(row.Tags, tags) { + foundRow = row + break + } + } + if foundRow == nil { + var gotTags [][]tag.Tag + for _, row := range rows { + gotTags = append(gotTags, row.Tags) + } + t.Fatalf("Failed to find row with keys %v among:\n%v", tags, gotTags) + return nil + } + return foundRow.Data.(*view.DistributionData) +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/example_test.go b/vendor/go.opencensus.io/plugin/ocgrpc/example_test.go new file mode 100644 index 0000000000..9c0ca7c9d0 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/example_test.go @@ -0,0 +1,50 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ocgrpc_test + +import ( + "log" + + "go.opencensus.io/plugin/ocgrpc" + "go.opencensus.io/stats/view" + "google.golang.org/grpc" +) + +func ExampleClientHandler() { + // Register views to collect data. + if err := view.Register(ocgrpc.DefaultClientViews...); err != nil { + log.Fatal(err) + } + + // Set up a connection to the server with the OpenCensus + // stats handler to enable stats and tracing. + conn, err := grpc.Dial("address", grpc.WithStatsHandler(&ocgrpc.ClientHandler{})) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + defer conn.Close() +} + +func ExampleServerHandler() { + // Register views to collect data. + if err := view.Register(ocgrpc.DefaultServerViews...); err != nil { + log.Fatal(err) + } + + // Set up a new server with the OpenCensus + // stats handler to enable stats and tracing. + s := grpc.NewServer(grpc.StatsHandler(&ocgrpc.ServerHandler{})) + _ = s // use s +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/grpc_test.go b/vendor/go.opencensus.io/plugin/ocgrpc/grpc_test.go new file mode 100644 index 0000000000..73b0f5c132 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/grpc_test.go @@ -0,0 +1,139 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ocgrpc + +import ( + "sync" + "testing" + "time" + + "go.opencensus.io/stats/view" + "golang.org/x/net/context" + "google.golang.org/grpc/metadata" + + "go.opencensus.io/trace" + + "google.golang.org/grpc/stats" +) + +func TestClientHandler(t *testing.T) { + ctx := context.Background() + te := &traceExporter{} + trace.RegisterExporter(te) + if err := view.Register(ClientSentMessagesPerRPCView); err != nil { + t.Fatal(err) + } + defer view.Unregister(ClientSentMessagesPerRPCView) + + ctx, _ = trace.StartSpan(ctx, "/foo", trace.WithSampler(trace.AlwaysSample())) + + var handler ClientHandler + ctx = handler.TagRPC(ctx, &stats.RPCTagInfo{ + FullMethodName: "/service.foo/method", + }) + handler.HandleRPC(ctx, &stats.Begin{ + Client: true, + BeginTime: time.Now(), + }) + handler.HandleRPC(ctx, &stats.End{ + Client: true, + EndTime: time.Now(), + }) + + stats, err := view.RetrieveData(ClientSentMessagesPerRPCView.Name) + if err != nil { + t.Fatal(err) + } + traces := te.buffer + + if got, want := len(stats), 1; got != want { + t.Errorf("Got %v stats; want %v", got, want) + } + if got, want := len(traces), 1; got != want { + t.Errorf("Got %v traces; want %v", got, want) + } +} + +func TestServerHandler(t *testing.T) { + tests := []struct { + name string + newTrace bool + expectTraces int + }{ + {"trust_metadata", false, 1}, + {"no_trust_metadata", true, 0}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + ctx := context.Background() + + handler := &ServerHandler{ + IsPublicEndpoint: test.newTrace, + StartOptions: trace.StartOptions{ + Sampler: trace.ProbabilitySampler(0.0), + }, + } + + te := &traceExporter{} + trace.RegisterExporter(te) + if err := view.Register(ServerCompletedRPCsView); err != nil { + t.Fatal(err) + } + + md := metadata.MD{ + "grpc-trace-bin": []string{string([]byte{0, 0, 62, 116, 14, 118, 117, 157, 126, 7, 114, 152, 102, 125, 235, 34, 114, 238, 1, 187, 201, 24, 210, 231, 20, 175, 241, 2, 1})}, + } + ctx = metadata.NewIncomingContext(ctx, md) + ctx = handler.TagRPC(ctx, &stats.RPCTagInfo{ + FullMethodName: "/service.foo/method", + }) + handler.HandleRPC(ctx, &stats.Begin{ + BeginTime: time.Now(), + }) + handler.HandleRPC(ctx, &stats.End{ + EndTime: time.Now(), + }) + + rows, err := view.RetrieveData(ServerCompletedRPCsView.Name) + if err != nil { + t.Fatal(err) + } + traces := te.buffer + + if got, want := len(rows), 1; got != want { + t.Errorf("Got %v rows; want %v", got, want) + } + if got, want := len(traces), test.expectTraces; got != want { + t.Errorf("Got %v traces; want %v", got, want) + } + + // Cleanup. + view.Unregister(ServerCompletedRPCsView) + }) + } +} + +type traceExporter struct { + mu sync.Mutex + buffer []*trace.SpanData +} + +func (e *traceExporter) ExportSpan(sd *trace.SpanData) { + e.mu.Lock() + e.buffer = append(e.buffer, sd) + e.mu.Unlock() +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/server.go b/vendor/go.opencensus.io/plugin/ocgrpc/server.go new file mode 100644 index 0000000000..b67b3e2be2 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/server.go @@ -0,0 +1,80 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ocgrpc + +import ( + "go.opencensus.io/trace" + "golang.org/x/net/context" + + "google.golang.org/grpc/stats" +) + +// ServerHandler implements gRPC stats.Handler recording OpenCensus stats and +// traces. Use with gRPC servers. +// +// When installed (see Example), tracing metadata is read from inbound RPCs +// by default. If no tracing metadata is present, or if the tracing metadata is +// present but the SpanContext isn't sampled, then a new trace may be started +// (as determined by Sampler). +type ServerHandler struct { + // IsPublicEndpoint may be set to true to always start a new trace around + // each RPC. Any SpanContext in the RPC metadata will be added as a linked + // span instead of making it the parent of the span created around the + // server RPC. + // + // Be aware that if you leave this false (the default) on a public-facing + // server, callers will be able to send tracing metadata in gRPC headers + // and trigger traces in your backend. + IsPublicEndpoint bool + + // StartOptions to use for to spans started around RPCs handled by this server. + // + // These will apply even if there is tracing metadata already + // present on the inbound RPC but the SpanContext is not sampled. This + // ensures that each service has some opportunity to be traced. If you would + // like to not add any additional traces for this gRPC service, set: + // + // StartOptions.Sampler = trace.ProbabilitySampler(0.0) + // + // StartOptions.SpanKind will always be set to trace.SpanKindServer + // for spans started by this handler. + StartOptions trace.StartOptions +} + +var _ stats.Handler = (*ServerHandler)(nil) + +// HandleConn exists to satisfy gRPC stats.Handler. +func (s *ServerHandler) HandleConn(ctx context.Context, cs stats.ConnStats) { + // no-op +} + +// TagConn exists to satisfy gRPC stats.Handler. +func (s *ServerHandler) TagConn(ctx context.Context, cti *stats.ConnTagInfo) context.Context { + // no-op + return ctx +} + +// HandleRPC implements per-RPC tracing and stats instrumentation. +func (s *ServerHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) { + traceHandleRPC(ctx, rs) + statsHandleRPC(ctx, rs) +} + +// TagRPC implements per-RPC context management. +func (s *ServerHandler) TagRPC(ctx context.Context, rti *stats.RPCTagInfo) context.Context { + ctx = s.traceTagRPC(ctx, rti) + ctx = s.statsTagRPC(ctx, rti) + return ctx +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/server_metrics.go b/vendor/go.opencensus.io/plugin/ocgrpc/server_metrics.go new file mode 100644 index 0000000000..609d9ed248 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/server_metrics.go @@ -0,0 +1,97 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package ocgrpc + +import ( + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +// The following variables are measures are recorded by ServerHandler: +var ( + ServerReceivedMessagesPerRPC = stats.Int64("grpc.io/server/received_messages_per_rpc", "Number of messages received in each RPC. Has value 1 for non-streaming RPCs.", stats.UnitDimensionless) + ServerReceivedBytesPerRPC = stats.Int64("grpc.io/server/received_bytes_per_rpc", "Total bytes received across all messages per RPC.", stats.UnitBytes) + ServerSentMessagesPerRPC = stats.Int64("grpc.io/server/sent_messages_per_rpc", "Number of messages sent in each RPC. Has value 1 for non-streaming RPCs.", stats.UnitDimensionless) + ServerSentBytesPerRPC = stats.Int64("grpc.io/server/sent_bytes_per_rpc", "Total bytes sent in across all response messages per RPC.", stats.UnitBytes) + ServerLatency = stats.Float64("grpc.io/server/server_latency", "Time between first byte of request received to last byte of response sent, or terminal error.", stats.UnitMilliseconds) +) + +// TODO(acetechnologist): This is temporary and will need to be replaced by a +// mechanism to load these defaults from a common repository/config shared by +// all supported languages. Likely a serialized protobuf of these defaults. + +// Predefined views may be registered to collect data for the above measures. +// As always, you may also define your own custom views over measures collected by this +// package. These are declared as a convenience only; none are registered by +// default. +var ( + ServerReceivedBytesPerRPCView = &view.View{ + Name: "grpc.io/server/received_bytes_per_rpc", + Description: "Distribution of received bytes per RPC, by method.", + Measure: ServerReceivedBytesPerRPC, + TagKeys: []tag.Key{KeyServerMethod}, + Aggregation: DefaultBytesDistribution, + } + + ServerSentBytesPerRPCView = &view.View{ + Name: "grpc.io/server/sent_bytes_per_rpc", + Description: "Distribution of total sent bytes per RPC, by method.", + Measure: ServerSentBytesPerRPC, + TagKeys: []tag.Key{KeyServerMethod}, + Aggregation: DefaultBytesDistribution, + } + + ServerLatencyView = &view.View{ + Name: "grpc.io/server/server_latency", + Description: "Distribution of server latency in milliseconds, by method.", + TagKeys: []tag.Key{KeyServerMethod}, + Measure: ServerLatency, + Aggregation: DefaultMillisecondsDistribution, + } + + ServerCompletedRPCsView = &view.View{ + Name: "grpc.io/server/completed_rpcs", + Description: "Count of RPCs by method and status.", + TagKeys: []tag.Key{KeyServerMethod, KeyServerStatus}, + Measure: ServerLatency, + Aggregation: view.Count(), + } + + ServerReceivedMessagesPerRPCView = &view.View{ + Name: "grpc.io/server/received_messages_per_rpc", + Description: "Distribution of messages received count per RPC, by method.", + TagKeys: []tag.Key{KeyServerMethod}, + Measure: ServerReceivedMessagesPerRPC, + Aggregation: DefaultMessageCountDistribution, + } + + ServerSentMessagesPerRPCView = &view.View{ + Name: "grpc.io/server/sent_messages_per_rpc", + Description: "Distribution of messages sent count per RPC, by method.", + TagKeys: []tag.Key{KeyServerMethod}, + Measure: ServerSentMessagesPerRPC, + Aggregation: DefaultMessageCountDistribution, + } +) + +// DefaultServerViews are the default server views provided by this package. +var DefaultServerViews = []*view.View{ + ServerReceivedBytesPerRPCView, + ServerSentBytesPerRPCView, + ServerLatencyView, + ServerCompletedRPCsView, +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/server_spec_test.go b/vendor/go.opencensus.io/plugin/ocgrpc/server_spec_test.go new file mode 100644 index 0000000000..f6ac5128a0 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/server_spec_test.go @@ -0,0 +1,146 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package ocgrpc + +import ( + "strings" + "testing" + + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" +) + +func TestSpecServerMeasures(t *testing.T) { + spec := ` +| Measure name | Unit | Description | +|------------------------------------------|------|-----------------------------------------------------------------------------------------------| +| grpc.io/server/received_messages_per_rpc | 1 | Number of messages received in each RPC. Has value 1 for non-streaming RPCs. | +| grpc.io/server/received_bytes_per_rpc | By | Total bytes received across all messages per RPC. | +| grpc.io/server/sent_messages_per_rpc | 1 | Number of messages sent in each RPC. Has value 1 for non-streaming RPCs. | +| grpc.io/server/sent_bytes_per_rpc | By | Total bytes sent in across all response messages per RPC. | +| grpc.io/server/server_latency | ms | Time between first byte of request received to last byte of response sent, or terminal error. |` + + lines := strings.Split(spec, "\n")[3:] + type measureDef struct { + name string + unit string + desc string + } + measureDefs := make([]measureDef, 0, len(lines)) + for _, line := range lines { + cols := colSep.Split(line, -1)[1:] + if len(cols) < 3 { + t.Fatalf("Invalid config line %#v", cols) + } + measureDefs = append(measureDefs, measureDef{cols[0], cols[1], cols[2]}) + } + + gotMeasures := []stats.Measure{ + ServerReceivedMessagesPerRPC, + ServerReceivedBytesPerRPC, + ServerSentMessagesPerRPC, + ServerSentBytesPerRPC, + ServerLatency, + } + + if got, want := len(gotMeasures), len(measureDefs); got != want { + t.Fatalf("len(gotMeasures) = %d; want %d", got, want) + } + + for i, m := range gotMeasures { + defn := measureDefs[i] + if got, want := m.Name(), defn.name; got != want { + t.Errorf("Name = %q; want %q", got, want) + } + if got, want := m.Unit(), defn.unit; got != want { + t.Errorf("%q: Unit = %q; want %q", defn.name, got, want) + } + if got, want := m.Description(), defn.desc; got != want { + t.Errorf("%q: Description = %q; want %q", defn.name, got, want) + } + } +} + +func TestSpecServerViews(t *testing.T) { + defaultViewsSpec := ` +| View name | Measure suffix | Aggregation | Tags suffix | +|---------------------------------------|------------------------|--------------|------------------------------| +| grpc.io/server/received_bytes_per_rpc | received_bytes_per_rpc | distribution | server_method | +| grpc.io/server/sent_bytes_per_rpc | sent_bytes_per_rpc | distribution | server_method | +| grpc.io/server/server_latency | server_latency | distribution | server_method | +| grpc.io/server/completed_rpcs | server_latency | count | server_method, server_status |` + + extraViewsSpec := ` +| View name | Measure suffix | Aggregation | Tags suffix | +|------------------------------------------|---------------------------|--------------|---------------| +| grpc.io/server/received_messages_per_rpc | received_messages_per_rpc | distribution | server_method | +| grpc.io/server/sent_messages_per_rpc | sent_messages_per_rpc | distribution | server_method |` + + lines := strings.Split(defaultViewsSpec, "\n")[3:] + lines = append(lines, strings.Split(extraViewsSpec, "\n")[3:]...) + type viewDef struct { + name string + measureSuffix string + aggregation string + tags string + } + viewDefs := make([]viewDef, 0, len(lines)) + for _, line := range lines { + cols := colSep.Split(line, -1)[1:] + if len(cols) < 4 { + t.Fatalf("Invalid config line %#v", cols) + } + viewDefs = append(viewDefs, viewDef{cols[0], cols[1], cols[2], cols[3]}) + } + + views := DefaultServerViews + views = append(views, ServerReceivedMessagesPerRPCView, ServerSentMessagesPerRPCView) + + if got, want := len(views), len(viewDefs); got != want { + t.Fatalf("len(gotMeasures) = %d; want %d", got, want) + } + + for i, v := range views { + defn := viewDefs[i] + if got, want := v.Name, defn.name; got != want { + t.Errorf("Name = %q; want %q", got, want) + } + if got, want := v.Measure.Name(), "grpc.io/server/"+defn.measureSuffix; got != want { + t.Errorf("%q: Measure.Name = %q; want %q", defn.name, got, want) + } + switch v.Aggregation.Type { + case view.AggTypeDistribution: + if got, want := "distribution", defn.aggregation; got != want { + t.Errorf("%q: Description = %q; want %q", defn.name, got, want) + } + case view.AggTypeCount: + if got, want := "count", defn.aggregation; got != want { + t.Errorf("%q: Description = %q; want %q", defn.name, got, want) + } + default: + t.Errorf("Invalid aggregation type") + } + wantTags := strings.Split(defn.tags, ", ") + if got, want := len(v.TagKeys), len(wantTags); got != want { + t.Errorf("len(TagKeys) = %d; want %d", got, want) + } + for j := range wantTags { + if got, want := v.TagKeys[j].Name(), "grpc_"+wantTags[j]; got != want { + t.Errorf("TagKeys[%d].Name() = %q; want %q", j, got, want) + } + } + } +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/server_stats_handler.go b/vendor/go.opencensus.io/plugin/ocgrpc/server_stats_handler.go new file mode 100644 index 0000000000..7847c1a912 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/server_stats_handler.go @@ -0,0 +1,63 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package ocgrpc + +import ( + "time" + + "golang.org/x/net/context" + + "go.opencensus.io/tag" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/stats" +) + +// statsTagRPC gets the metadata from gRPC context, extracts the encoded tags from +// it and creates a new tag.Map and puts them into the returned context. +func (h *ServerHandler) statsTagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context { + startTime := time.Now() + if info == nil { + if grpclog.V(2) { + grpclog.Infof("opencensus: TagRPC called with nil info.") + } + return ctx + } + d := &rpcData{ + startTime: startTime, + method: info.FullMethodName, + } + propagated := h.extractPropagatedTags(ctx) + ctx = tag.NewContext(ctx, propagated) + ctx, _ = tag.New(ctx, tag.Upsert(KeyServerMethod, methodName(info.FullMethodName))) + return context.WithValue(ctx, rpcDataKey, d) +} + +// extractPropagatedTags creates a new tag map containing the tags extracted from the +// gRPC metadata. +func (h *ServerHandler) extractPropagatedTags(ctx context.Context) *tag.Map { + buf := stats.Tags(ctx) + if buf == nil { + return nil + } + propagated, err := tag.Decode(buf) + if err != nil { + if grpclog.V(2) { + grpclog.Warningf("opencensus: Failed to decode tags from gRPC metadata failed to decode: %v", err) + } + return nil + } + return propagated +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/server_stats_handler_test.go b/vendor/go.opencensus.io/plugin/ocgrpc/server_stats_handler_test.go new file mode 100644 index 0000000000..1b96edf880 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/server_stats_handler_test.go @@ -0,0 +1,336 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package ocgrpc + +import ( + "testing" + + "go.opencensus.io/trace" + "golang.org/x/net/context" + + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/stats" + "google.golang.org/grpc/status" +) + +func TestServerDefaultCollections(t *testing.T) { + k1, _ := tag.NewKey("k1") + k2, _ := tag.NewKey("k2") + + type tagPair struct { + k tag.Key + v string + } + + type wantData struct { + v func() *view.View + rows []*view.Row + } + type rpc struct { + tags []tagPair + tagInfo *stats.RPCTagInfo + inPayloads []*stats.InPayload + outPayloads []*stats.OutPayload + end *stats.End + } + + type testCase struct { + label string + rpcs []*rpc + wants []*wantData + } + + tcs := []testCase{ + { + "1", + []*rpc{ + { + []tagPair{{k1, "v1"}}, + &stats.RPCTagInfo{FullMethodName: "/package.service/method"}, + []*stats.InPayload{ + {Length: 10}, + }, + []*stats.OutPayload{ + {Length: 10}, + }, + &stats.End{Error: nil}, + }, + }, + []*wantData{ + { + func() *view.View { return ServerReceivedMessagesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyServerMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 1, 1, 1, 0), + }, + }, + }, + { + func() *view.View { return ServerSentMessagesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyServerMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 1, 1, 1, 0), + }, + }, + }, + { + func() *view.View { return ServerReceivedBytesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyServerMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 10, 10, 10, 0), + }, + }, + }, + { + func() *view.View { return ServerSentBytesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyServerMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 10, 10, 10, 0), + }, + }, + }, + }, + }, + { + "2", + []*rpc{ + { + []tagPair{{k1, "v1"}}, + &stats.RPCTagInfo{FullMethodName: "/package.service/method"}, + []*stats.InPayload{ + {Length: 10}, + }, + []*stats.OutPayload{ + {Length: 10}, + {Length: 10}, + {Length: 10}, + }, + &stats.End{Error: nil}, + }, + { + []tagPair{{k1, "v11"}}, + &stats.RPCTagInfo{FullMethodName: "/package.service/method"}, + []*stats.InPayload{ + {Length: 10}, + {Length: 10}, + }, + []*stats.OutPayload{ + {Length: 10}, + {Length: 10}, + }, + &stats.End{Error: status.Error(codes.Canceled, "canceled")}, + }, + }, + []*wantData{ + { + func() *view.View { return ServerReceivedMessagesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyServerMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 2, 1, 2, 1.5, 0.5), + }, + }, + }, + { + func() *view.View { return ServerSentMessagesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyServerMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 2, 2, 3, 2.5, 0.5), + }, + }, + }, + }, + }, + { + "3", + []*rpc{ + { + []tagPair{{k1, "v1"}}, + &stats.RPCTagInfo{FullMethodName: "/package.service/method"}, + []*stats.InPayload{ + {Length: 1}, + }, + []*stats.OutPayload{ + {Length: 1}, + {Length: 1024}, + {Length: 65536}, + }, + &stats.End{Error: nil}, + }, + { + []tagPair{{k1, "v1"}, {k2, "v2"}}, + &stats.RPCTagInfo{FullMethodName: "/package.service/method"}, + []*stats.InPayload{ + {Length: 1024}, + }, + []*stats.OutPayload{ + {Length: 4096}, + {Length: 16384}, + }, + &stats.End{Error: status.Error(codes.Aborted, "aborted")}, + }, + { + []tagPair{{k1, "v11"}, {k2, "v22"}}, + &stats.RPCTagInfo{FullMethodName: "/package.service/method"}, + []*stats.InPayload{ + {Length: 2048}, + {Length: 16384}, + }, + []*stats.OutPayload{ + {Length: 2048}, + {Length: 4096}, + {Length: 16384}, + }, + &stats.End{Error: status.Error(codes.Canceled, "canceled")}, + }, + }, + []*wantData{ + { + func() *view.View { return ServerReceivedMessagesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyServerMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 3, 1, 2, 1.333333333, 0.333333333*2), + }, + }, + }, + { + func() *view.View { return ServerSentMessagesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyServerMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 3, 2, 3, 2.666666666, 0.333333333*2), + }, + }, + }, + { + func() *view.View { return ServerReceivedBytesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyServerMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 3, 1, 18432, 6485.6666667, 2.1459558466666667e+08), + }, + }, + }, + { + func() *view.View { return ServerSentBytesPerRPCView }, + []*view.Row{ + { + Tags: []tag.Tag{ + {Key: KeyServerMethod, Value: "package.service/method"}, + }, + Data: newDistributionData([]int64{0, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0}, 3, 20480, 66561, 36523, 1.355519318e+09), + }, + }, + }, + }, + }, + } + + views := append(DefaultServerViews[:], ServerReceivedMessagesPerRPCView, ServerSentMessagesPerRPCView) + + for _, tc := range tcs { + if err := view.Register(views...); err != nil { + t.Fatal(err) + } + + h := &ServerHandler{} + h.StartOptions.Sampler = trace.NeverSample() + for _, rpc := range tc.rpcs { + mods := []tag.Mutator{} + for _, t := range rpc.tags { + mods = append(mods, tag.Upsert(t.k, t.v)) + } + ctx, err := tag.New(context.Background(), mods...) + if err != nil { + t.Errorf("%q: NewMap = %v", tc.label, err) + } + encoded := tag.Encode(tag.FromContext(ctx)) + ctx = stats.SetTags(context.Background(), encoded) + ctx = h.TagRPC(ctx, rpc.tagInfo) + + for _, in := range rpc.inPayloads { + h.HandleRPC(ctx, in) + } + for _, out := range rpc.outPayloads { + h.HandleRPC(ctx, out) + } + h.HandleRPC(ctx, rpc.end) + } + + for _, wantData := range tc.wants { + gotRows, err := view.RetrieveData(wantData.v().Name) + if err != nil { + t.Errorf("%q: RetrieveData (%q) = %v", tc.label, wantData.v().Name, err) + continue + } + + for _, gotRow := range gotRows { + if !containsRow(wantData.rows, gotRow) { + t.Errorf("%q: unwanted row for view %q: %v", tc.label, wantData.v().Name, gotRow) + break + } + } + + for _, wantRow := range wantData.rows { + if !containsRow(gotRows, wantRow) { + t.Errorf("%q: missing row for view %q: %v", tc.label, wantData.v().Name, wantRow) + break + } + } + } + + // Unregister views to cleanup. + view.Unregister(views...) + } +} + +func newDistributionData(countPerBucket []int64, count int64, min, max, mean, sumOfSquaredDev float64) *view.DistributionData { + return &view.DistributionData{ + Count: count, + Min: min, + Max: max, + Mean: mean, + SumOfSquaredDev: sumOfSquaredDev, + CountPerBucket: countPerBucket, + } +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/stats_common.go b/vendor/go.opencensus.io/plugin/ocgrpc/stats_common.go new file mode 100644 index 0000000000..1737809e72 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/stats_common.go @@ -0,0 +1,208 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package ocgrpc + +import ( + "context" + "strconv" + "strings" + "sync/atomic" + "time" + + ocstats "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/stats" + "google.golang.org/grpc/status" +) + +type grpcInstrumentationKey string + +// rpcData holds the instrumentation RPC data that is needed between the start +// and end of an call. It holds the info that this package needs to keep track +// of between the various GRPC events. +type rpcData struct { + // reqCount and respCount has to be the first words + // in order to be 64-aligned on 32-bit architectures. + sentCount, sentBytes, recvCount, recvBytes int64 // access atomically + + // startTime represents the time at which TagRPC was invoked at the + // beginning of an RPC. It is an appoximation of the time when the + // application code invoked GRPC code. + startTime time.Time + method string +} + +// The following variables define the default hard-coded auxiliary data used by +// both the default GRPC client and GRPC server metrics. +var ( + DefaultBytesDistribution = view.Distribution(0, 1024, 2048, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, 268435456, 1073741824, 4294967296) + DefaultMillisecondsDistribution = view.Distribution(0, 0.01, 0.05, 0.1, 0.3, 0.6, 0.8, 1, 2, 3, 4, 5, 6, 8, 10, 13, 16, 20, 25, 30, 40, 50, 65, 80, 100, 130, 160, 200, 250, 300, 400, 500, 650, 800, 1000, 2000, 5000, 10000, 20000, 50000, 100000) + DefaultMessageCountDistribution = view.Distribution(0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536) +) + +// Server tags are applied to the context used to process each RPC, as well as +// the measures at the end of each RPC. +var ( + KeyServerMethod, _ = tag.NewKey("grpc_server_method") + KeyServerStatus, _ = tag.NewKey("grpc_server_status") +) + +// Client tags are applied to measures at the end of each RPC. +var ( + KeyClientMethod, _ = tag.NewKey("grpc_client_method") + KeyClientStatus, _ = tag.NewKey("grpc_client_status") +) + +var ( + rpcDataKey = grpcInstrumentationKey("opencensus-rpcData") +) + +func methodName(fullname string) string { + return strings.TrimLeft(fullname, "/") +} + +// statsHandleRPC processes the RPC events. +func statsHandleRPC(ctx context.Context, s stats.RPCStats) { + switch st := s.(type) { + case *stats.Begin, *stats.OutHeader, *stats.InHeader, *stats.InTrailer, *stats.OutTrailer: + // do nothing for client + case *stats.OutPayload: + handleRPCOutPayload(ctx, st) + case *stats.InPayload: + handleRPCInPayload(ctx, st) + case *stats.End: + handleRPCEnd(ctx, st) + default: + grpclog.Infof("unexpected stats: %T", st) + } +} + +func handleRPCOutPayload(ctx context.Context, s *stats.OutPayload) { + d, ok := ctx.Value(rpcDataKey).(*rpcData) + if !ok { + if grpclog.V(2) { + grpclog.Infoln("Failed to retrieve *rpcData from context.") + } + return + } + + atomic.AddInt64(&d.sentBytes, int64(s.Length)) + atomic.AddInt64(&d.sentCount, 1) +} + +func handleRPCInPayload(ctx context.Context, s *stats.InPayload) { + d, ok := ctx.Value(rpcDataKey).(*rpcData) + if !ok { + if grpclog.V(2) { + grpclog.Infoln("Failed to retrieve *rpcData from context.") + } + return + } + + atomic.AddInt64(&d.recvBytes, int64(s.Length)) + atomic.AddInt64(&d.recvCount, 1) +} + +func handleRPCEnd(ctx context.Context, s *stats.End) { + d, ok := ctx.Value(rpcDataKey).(*rpcData) + if !ok { + if grpclog.V(2) { + grpclog.Infoln("Failed to retrieve *rpcData from context.") + } + return + } + + elapsedTime := time.Since(d.startTime) + + var st string + if s.Error != nil { + s, ok := status.FromError(s.Error) + if ok { + st = statusCodeToString(s) + } + } else { + st = "OK" + } + + latencyMillis := float64(elapsedTime) / float64(time.Millisecond) + if s.Client { + ocstats.RecordWithTags(ctx, + []tag.Mutator{ + tag.Upsert(KeyClientMethod, methodName(d.method)), + tag.Upsert(KeyClientStatus, st), + }, + ClientSentBytesPerRPC.M(atomic.LoadInt64(&d.sentBytes)), + ClientSentMessagesPerRPC.M(atomic.LoadInt64(&d.sentCount)), + ClientReceivedMessagesPerRPC.M(atomic.LoadInt64(&d.recvCount)), + ClientReceivedBytesPerRPC.M(atomic.LoadInt64(&d.recvBytes)), + ClientRoundtripLatency.M(latencyMillis)) + } else { + ocstats.RecordWithTags(ctx, + []tag.Mutator{ + tag.Upsert(KeyServerStatus, st), + }, + ServerSentBytesPerRPC.M(atomic.LoadInt64(&d.sentBytes)), + ServerSentMessagesPerRPC.M(atomic.LoadInt64(&d.sentCount)), + ServerReceivedMessagesPerRPC.M(atomic.LoadInt64(&d.recvCount)), + ServerReceivedBytesPerRPC.M(atomic.LoadInt64(&d.recvBytes)), + ServerLatency.M(latencyMillis)) + } +} + +func statusCodeToString(s *status.Status) string { + // see https://github.com/grpc/grpc/blob/master/doc/statuscodes.md + switch c := s.Code(); c { + case codes.OK: + return "OK" + case codes.Canceled: + return "CANCELLED" + case codes.Unknown: + return "UNKNOWN" + case codes.InvalidArgument: + return "INVALID_ARGUMENT" + case codes.DeadlineExceeded: + return "DEADLINE_EXCEEDED" + case codes.NotFound: + return "NOT_FOUND" + case codes.AlreadyExists: + return "ALREADY_EXISTS" + case codes.PermissionDenied: + return "PERMISSION_DENIED" + case codes.ResourceExhausted: + return "RESOURCE_EXHAUSTED" + case codes.FailedPrecondition: + return "FAILED_PRECONDITION" + case codes.Aborted: + return "ABORTED" + case codes.OutOfRange: + return "OUT_OF_RANGE" + case codes.Unimplemented: + return "UNIMPLEMENTED" + case codes.Internal: + return "INTERNAL" + case codes.Unavailable: + return "UNAVAILABLE" + case codes.DataLoss: + return "DATA_LOSS" + case codes.Unauthenticated: + return "UNAUTHENTICATED" + default: + return "CODE_" + strconv.FormatInt(int64(c), 10) + } +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/trace_common.go b/vendor/go.opencensus.io/plugin/ocgrpc/trace_common.go new file mode 100644 index 0000000000..720f381c27 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/trace_common.go @@ -0,0 +1,107 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ocgrpc + +import ( + "strings" + + "google.golang.org/grpc/codes" + + "go.opencensus.io/trace" + "go.opencensus.io/trace/propagation" + "golang.org/x/net/context" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/stats" + "google.golang.org/grpc/status" +) + +const traceContextKey = "grpc-trace-bin" + +// TagRPC creates a new trace span for the client side of the RPC. +// +// It returns ctx with the new trace span added and a serialization of the +// SpanContext added to the outgoing gRPC metadata. +func (c *ClientHandler) traceTagRPC(ctx context.Context, rti *stats.RPCTagInfo) context.Context { + name := strings.TrimPrefix(rti.FullMethodName, "/") + name = strings.Replace(name, "/", ".", -1) + ctx, span := trace.StartSpan(ctx, name, + trace.WithSampler(c.StartOptions.Sampler), + trace.WithSpanKind(trace.SpanKindClient)) // span is ended by traceHandleRPC + traceContextBinary := propagation.Binary(span.SpanContext()) + return metadata.AppendToOutgoingContext(ctx, traceContextKey, string(traceContextBinary)) +} + +// TagRPC creates a new trace span for the server side of the RPC. +// +// It checks the incoming gRPC metadata in ctx for a SpanContext, and if +// it finds one, uses that SpanContext as the parent context of the new span. +// +// It returns ctx, with the new trace span added. +func (s *ServerHandler) traceTagRPC(ctx context.Context, rti *stats.RPCTagInfo) context.Context { + md, _ := metadata.FromIncomingContext(ctx) + name := strings.TrimPrefix(rti.FullMethodName, "/") + name = strings.Replace(name, "/", ".", -1) + traceContext := md[traceContextKey] + var ( + parent trace.SpanContext + haveParent bool + ) + if len(traceContext) > 0 { + // Metadata with keys ending in -bin are actually binary. They are base64 + // encoded before being put on the wire, see: + // https://github.com/grpc/grpc-go/blob/08d6261/Documentation/grpc-metadata.md#storing-binary-data-in-metadata + traceContextBinary := []byte(traceContext[0]) + parent, haveParent = propagation.FromBinary(traceContextBinary) + if haveParent && !s.IsPublicEndpoint { + ctx, _ := trace.StartSpanWithRemoteParent(ctx, name, parent, + trace.WithSpanKind(trace.SpanKindServer), + trace.WithSampler(s.StartOptions.Sampler), + ) + return ctx + } + } + ctx, span := trace.StartSpan(ctx, name, + trace.WithSpanKind(trace.SpanKindServer), + trace.WithSampler(s.StartOptions.Sampler)) + if haveParent { + span.AddLink(trace.Link{TraceID: parent.TraceID, SpanID: parent.SpanID, Type: trace.LinkTypeChild}) + } + return ctx +} + +func traceHandleRPC(ctx context.Context, rs stats.RPCStats) { + span := trace.FromContext(ctx) + // TODO: compressed and uncompressed sizes are not populated in every message. + switch rs := rs.(type) { + case *stats.Begin: + span.AddAttributes( + trace.BoolAttribute("Client", rs.Client), + trace.BoolAttribute("FailFast", rs.FailFast)) + case *stats.InPayload: + span.AddMessageReceiveEvent(0 /* TODO: messageID */, int64(rs.Length), int64(rs.WireLength)) + case *stats.OutPayload: + span.AddMessageSendEvent(0, int64(rs.Length), int64(rs.WireLength)) + case *stats.End: + if rs.Error != nil { + s, ok := status.FromError(rs.Error) + if ok { + span.SetStatus(trace.Status{Code: int32(s.Code()), Message: s.Message()}) + } else { + span.SetStatus(trace.Status{Code: int32(codes.Internal), Message: rs.Error.Error()}) + } + } + span.End() + } +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/trace_common_test.go b/vendor/go.opencensus.io/plugin/ocgrpc/trace_common_test.go new file mode 100644 index 0000000000..9e590d994b --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/trace_common_test.go @@ -0,0 +1,46 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ocgrpc + +import ( + "testing" + + "go.opencensus.io/trace" + "golang.org/x/net/context" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/stats" +) + +func TestClientHandler_traceTagRPC(t *testing.T) { + ch := &ClientHandler{} + ch.StartOptions.Sampler = trace.AlwaysSample() + rti := &stats.RPCTagInfo{ + FullMethodName: "xxx", + } + ctx := context.Background() + ctx = ch.traceTagRPC(ctx, rti) + + span := trace.FromContext(ctx) + if span == nil { + t.Fatal("expected span, got nil") + } + if !span.IsRecordingEvents() { + t.Errorf("span should be sampled") + } + md, ok := metadata.FromOutgoingContext(ctx) + if !ok || len(md) == 0 || len(md[traceContextKey]) == 0 { + t.Fatal("no metadata") + } +} diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/trace_test.go b/vendor/go.opencensus.io/plugin/ocgrpc/trace_test.go new file mode 100644 index 0000000000..7c2243db76 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ocgrpc/trace_test.go @@ -0,0 +1,233 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ocgrpc_test + +import ( + "io" + "testing" + "time" + + "go.opencensus.io/internal/testpb" + "go.opencensus.io/trace" + "golang.org/x/net/context" +) + +type testExporter struct { + ch chan *trace.SpanData +} + +func (t *testExporter) ExportSpan(s *trace.SpanData) { + go func() { t.ch <- s }() +} + +func TestStreaming(t *testing.T) { + trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) + te := testExporter{make(chan *trace.SpanData)} + trace.RegisterExporter(&te) + defer trace.UnregisterExporter(&te) + + client, cleanup := testpb.NewTestClient(t) + + stream, err := client.Multiple(context.Background()) + if err != nil { + t.Fatalf("Call failed: %v", err) + } + + err = stream.Send(&testpb.FooRequest{}) + if err != nil { + t.Fatalf("Couldn't send streaming request: %v", err) + } + stream.CloseSend() + + for { + _, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + t.Errorf("stream.Recv() = %v; want no errors", err) + } + } + + cleanup() + + s1 := <-te.ch + s2 := <-te.ch + + checkSpanData(t, s1, s2, "testpb.Foo.Multiple", true) + + select { + case <-te.ch: + t.Fatal("received extra exported spans") + case <-time.After(time.Second / 10): + } +} + +func TestStreamingFail(t *testing.T) { + trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) + te := testExporter{make(chan *trace.SpanData)} + trace.RegisterExporter(&te) + defer trace.UnregisterExporter(&te) + + client, cleanup := testpb.NewTestClient(t) + + stream, err := client.Multiple(context.Background()) + if err != nil { + t.Fatalf("Call failed: %v", err) + } + + err = stream.Send(&testpb.FooRequest{Fail: true}) + if err != nil { + t.Fatalf("Couldn't send streaming request: %v", err) + } + stream.CloseSend() + + for { + _, err := stream.Recv() + if err == nil || err == io.EOF { + t.Errorf("stream.Recv() = %v; want errors", err) + } else { + break + } + } + + s1 := <-te.ch + s2 := <-te.ch + + checkSpanData(t, s1, s2, "testpb.Foo.Multiple", false) + cleanup() + + select { + case <-te.ch: + t.Fatal("received extra exported spans") + case <-time.After(time.Second / 10): + } +} + +func TestSingle(t *testing.T) { + trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) + te := testExporter{make(chan *trace.SpanData)} + trace.RegisterExporter(&te) + defer trace.UnregisterExporter(&te) + + client, cleanup := testpb.NewTestClient(t) + + _, err := client.Single(context.Background(), &testpb.FooRequest{}) + if err != nil { + t.Fatalf("Couldn't send request: %v", err) + } + + s1 := <-te.ch + s2 := <-te.ch + + checkSpanData(t, s1, s2, "testpb.Foo.Single", true) + cleanup() + + select { + case <-te.ch: + t.Fatal("received extra exported spans") + case <-time.After(time.Second / 10): + } +} + +func TestServerSpanDuration(t *testing.T) { + client, cleanup := testpb.NewTestClient(t) + defer cleanup() + + te := testExporter{make(chan *trace.SpanData, 100)} + trace.RegisterExporter(&te) + defer trace.UnregisterExporter(&te) + + trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) + + ctx := context.Background() + const sleep = 100 * time.Millisecond + client.Single(ctx, &testpb.FooRequest{SleepNanos: int64(sleep)}) + +loop: + for { + select { + case span := <-te.ch: + if span.SpanKind != trace.SpanKindServer { + continue loop + } + if got, want := span.EndTime.Sub(span.StartTime), sleep; got < want { + t.Errorf("span duration = %dns; want at least %dns", got, want) + } + break loop + default: + t.Fatal("no more spans") + } + } +} + +func TestSingleFail(t *testing.T) { + trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) + te := testExporter{make(chan *trace.SpanData)} + trace.RegisterExporter(&te) + defer trace.UnregisterExporter(&te) + + client, cleanup := testpb.NewTestClient(t) + + _, err := client.Single(context.Background(), &testpb.FooRequest{Fail: true}) + if err == nil { + t.Fatalf("Got nil error from request, want non-nil") + } + + s1 := <-te.ch + s2 := <-te.ch + + checkSpanData(t, s1, s2, "testpb.Foo.Single", false) + cleanup() + + select { + case <-te.ch: + t.Fatal("received extra exported spans") + case <-time.After(time.Second / 10): + } +} + +func checkSpanData(t *testing.T, s1, s2 *trace.SpanData, methodName string, success bool) { + t.Helper() + + if s1.SpanKind == trace.SpanKindServer { + s1, s2 = s2, s1 + } + + if got, want := s1.Name, methodName; got != want { + t.Errorf("Got name %q want %q", got, want) + } + if got, want := s2.Name, methodName; got != want { + t.Errorf("Got name %q want %q", got, want) + } + if got, want := s2.SpanContext.TraceID, s1.SpanContext.TraceID; got != want { + t.Errorf("Got trace IDs %s and %s, want them equal", got, want) + } + if got, want := s2.ParentSpanID, s1.SpanContext.SpanID; got != want { + t.Errorf("Got ParentSpanID %s, want %s", got, want) + } + if got := (s1.Status.Code == 0); got != success { + t.Errorf("Got success=%t want %t", got, success) + } + if got := (s2.Status.Code == 0); got != success { + t.Errorf("Got success=%t want %t", got, success) + } + if s1.HasRemoteParent { + t.Errorf("Got HasRemoteParent=%t, want false", s1.HasRemoteParent) + } + if !s2.HasRemoteParent { + t.Errorf("Got HasRemoteParent=%t, want true", s2.HasRemoteParent) + } +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/client.go b/vendor/go.opencensus.io/plugin/ochttp/client.go new file mode 100644 index 0000000000..da815b2a73 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/client.go @@ -0,0 +1,117 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp + +import ( + "net/http" + "net/http/httptrace" + + "go.opencensus.io/trace" + "go.opencensus.io/trace/propagation" +) + +// Transport is an http.RoundTripper that instruments all outgoing requests with +// OpenCensus stats and tracing. +// +// The zero value is intended to be a useful default, but for +// now it's recommended that you explicitly set Propagation, since the default +// for this may change. +type Transport struct { + // Base may be set to wrap another http.RoundTripper that does the actual + // requests. By default http.DefaultTransport is used. + // + // If base HTTP roundtripper implements CancelRequest, + // the returned round tripper will be cancelable. + Base http.RoundTripper + + // Propagation defines how traces are propagated. If unspecified, a default + // (currently B3 format) will be used. + Propagation propagation.HTTPFormat + + // StartOptions are applied to the span started by this Transport around each + // request. + // + // StartOptions.SpanKind will always be set to trace.SpanKindClient + // for spans started by this transport. + StartOptions trace.StartOptions + + // GetStartOptions allows to set start options per request. If set, + // StartOptions is going to be ignored. + GetStartOptions func(*http.Request) trace.StartOptions + + // NameFromRequest holds the function to use for generating the span name + // from the information found in the outgoing HTTP Request. By default the + // name equals the URL Path. + FormatSpanName func(*http.Request) string + + // NewClientTrace may be set to a function allowing the current *trace.Span + // to be annotated with HTTP request event information emitted by the + // httptrace package. + NewClientTrace func(*http.Request, *trace.Span) *httptrace.ClientTrace + + // TODO: Implement tag propagation for HTTP. +} + +// RoundTrip implements http.RoundTripper, delegating to Base and recording stats and traces for the request. +func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + rt := t.base() + if isHealthEndpoint(req.URL.Path) { + return rt.RoundTrip(req) + } + // TODO: remove excessive nesting of http.RoundTrippers here. + format := t.Propagation + if format == nil { + format = defaultFormat + } + spanNameFormatter := t.FormatSpanName + if spanNameFormatter == nil { + spanNameFormatter = spanNameFromURL + } + + startOpts := t.StartOptions + if t.GetStartOptions != nil { + startOpts = t.GetStartOptions(req) + } + + rt = &traceTransport{ + base: rt, + format: format, + startOptions: trace.StartOptions{ + Sampler: startOpts.Sampler, + SpanKind: trace.SpanKindClient, + }, + formatSpanName: spanNameFormatter, + newClientTrace: t.NewClientTrace, + } + rt = statsTransport{base: rt} + return rt.RoundTrip(req) +} + +func (t *Transport) base() http.RoundTripper { + if t.Base != nil { + return t.Base + } + return http.DefaultTransport +} + +// CancelRequest cancels an in-flight request by closing its connection. +func (t *Transport) CancelRequest(req *http.Request) { + type canceler interface { + CancelRequest(*http.Request) + } + if cr, ok := t.base().(canceler); ok { + cr.CancelRequest(req) + } +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/client_stats.go b/vendor/go.opencensus.io/plugin/ochttp/client_stats.go new file mode 100644 index 0000000000..066ebb87f8 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/client_stats.go @@ -0,0 +1,135 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp + +import ( + "context" + "io" + "net/http" + "strconv" + "sync" + "time" + + "go.opencensus.io/stats" + "go.opencensus.io/tag" +) + +// statsTransport is an http.RoundTripper that collects stats for the outgoing requests. +type statsTransport struct { + base http.RoundTripper +} + +// RoundTrip implements http.RoundTripper, delegating to Base and recording stats for the request. +func (t statsTransport) RoundTrip(req *http.Request) (*http.Response, error) { + ctx, _ := tag.New(req.Context(), + tag.Upsert(KeyClientHost, req.URL.Host), + tag.Upsert(Host, req.URL.Host), + tag.Upsert(KeyClientPath, req.URL.Path), + tag.Upsert(Path, req.URL.Path), + tag.Upsert(KeyClientMethod, req.Method), + tag.Upsert(Method, req.Method)) + req = req.WithContext(ctx) + track := &tracker{ + start: time.Now(), + ctx: ctx, + } + if req.Body == nil { + // TODO: Handle cases where ContentLength is not set. + track.reqSize = -1 + } else if req.ContentLength > 0 { + track.reqSize = req.ContentLength + } + stats.Record(ctx, ClientRequestCount.M(1)) + + // Perform request. + resp, err := t.base.RoundTrip(req) + + if err != nil { + track.statusCode = http.StatusInternalServerError + track.end() + } else { + track.statusCode = resp.StatusCode + if resp.Body == nil { + track.end() + } else { + track.body = resp.Body + resp.Body = track + } + } + return resp, err +} + +// CancelRequest cancels an in-flight request by closing its connection. +func (t statsTransport) CancelRequest(req *http.Request) { + type canceler interface { + CancelRequest(*http.Request) + } + if cr, ok := t.base.(canceler); ok { + cr.CancelRequest(req) + } +} + +type tracker struct { + ctx context.Context + respSize int64 + reqSize int64 + start time.Time + body io.ReadCloser + statusCode int + endOnce sync.Once +} + +var _ io.ReadCloser = (*tracker)(nil) + +func (t *tracker) end() { + t.endOnce.Do(func() { + latencyMs := float64(time.Since(t.start)) / float64(time.Millisecond) + m := []stats.Measurement{ + ClientSentBytes.M(t.reqSize), + ClientReceivedBytes.M(t.respSize), + ClientRoundtripLatency.M(latencyMs), + ClientLatency.M(latencyMs), + ClientResponseBytes.M(t.respSize), + } + if t.reqSize >= 0 { + m = append(m, ClientRequestBytes.M(t.reqSize)) + } + + stats.RecordWithTags(t.ctx, []tag.Mutator{ + tag.Upsert(StatusCode, strconv.Itoa(t.statusCode)), + tag.Upsert(KeyClientStatus, strconv.Itoa(t.statusCode)), + }, m...) + }) +} + +func (t *tracker) Read(b []byte) (int, error) { + n, err := t.body.Read(b) + switch err { + case nil: + t.respSize += int64(n) + return n, nil + case io.EOF: + t.end() + } + return n, err +} + +func (t *tracker) Close() error { + // Invoking endSpan on Close will help catch the cases + // in which a read returned a non-nil error, we set the + // span status but didn't end the span. + t.end() + return t.body.Close() +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/client_test.go b/vendor/go.opencensus.io/plugin/ochttp/client_test.go new file mode 100644 index 0000000000..97d15ab97b --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/client_test.go @@ -0,0 +1,316 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + + "go.opencensus.io/plugin/ochttp" + "go.opencensus.io/stats/view" + "go.opencensus.io/trace" +) + +const reqCount = 5 + +func TestClientNew(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + resp.Write([]byte("Hello, world!")) + })) + defer server.Close() + + if err := view.Register( + ochttp.ClientSentBytesDistribution, + ochttp.ClientReceivedBytesDistribution, + ochttp.ClientRoundtripLatencyDistribution, + ochttp.ClientCompletedCount, + ); err != nil { + t.Fatalf("Failed to register ochttp.DefaultClientViews error: %v", err) + } + + views := []string{ + "opencensus.io/http/client/sent_bytes", + "opencensus.io/http/client/received_bytes", + "opencensus.io/http/client/roundtrip_latency", + "opencensus.io/http/client/completed_count", + } + for _, name := range views { + v := view.Find(name) + if v == nil { + t.Errorf("view not found %q", name) + continue + } + } + + var wg sync.WaitGroup + var tr ochttp.Transport + errs := make(chan error, reqCount) + wg.Add(reqCount) + + for i := 0; i < reqCount; i++ { + go func() { + defer wg.Done() + req, err := http.NewRequest("POST", server.URL, strings.NewReader("req-body")) + if err != nil { + errs <- fmt.Errorf("error creating request: %v", err) + } + resp, err := tr.RoundTrip(req) + if err != nil { + errs <- fmt.Errorf("response error: %v", err) + } + if err := resp.Body.Close(); err != nil { + errs <- fmt.Errorf("error closing response body: %v", err) + } + if got, want := resp.StatusCode, 200; got != want { + errs <- fmt.Errorf("resp.StatusCode=%d; wantCount %d", got, want) + } + }() + } + + go func() { + wg.Wait() + close(errs) + }() + + for err := range errs { + if err != nil { + t.Fatal(err) + } + } + + for _, viewName := range views { + v := view.Find(viewName) + if v == nil { + t.Errorf("view not found %q", viewName) + continue + } + rows, err := view.RetrieveData(v.Name) + if err != nil { + t.Error(err) + continue + } + if got, want := len(rows), 1; got != want { + t.Errorf("len(%q) = %d; want %d", viewName, got, want) + continue + } + data := rows[0].Data + var count int64 + switch data := data.(type) { + case *view.CountData: + count = data.Value + case *view.DistributionData: + count = data.Count + default: + t.Errorf("Unkown data type: %v", data) + continue + } + if got := count; got != reqCount { + t.Fatalf("%s = %d; want %d", viewName, got, reqCount) + } + } +} + +func TestClientOld(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + resp.Write([]byte("Hello, world!")) + })) + defer server.Close() + + if err := view.Register(ochttp.DefaultClientViews...); err != nil { + t.Fatalf("Failed to register ochttp.DefaultClientViews error: %v", err) + } + + views := []string{ + "opencensus.io/http/client/request_count", + "opencensus.io/http/client/latency", + "opencensus.io/http/client/request_bytes", + "opencensus.io/http/client/response_bytes", + } + for _, name := range views { + v := view.Find(name) + if v == nil { + t.Errorf("view not found %q", name) + continue + } + } + + var wg sync.WaitGroup + var tr ochttp.Transport + errs := make(chan error, reqCount) + wg.Add(reqCount) + + for i := 0; i < reqCount; i++ { + go func() { + defer wg.Done() + req, err := http.NewRequest("POST", server.URL, strings.NewReader("req-body")) + if err != nil { + errs <- fmt.Errorf("error creating request: %v", err) + } + resp, err := tr.RoundTrip(req) + if err != nil { + errs <- fmt.Errorf("response error: %v", err) + } + if err := resp.Body.Close(); err != nil { + errs <- fmt.Errorf("error closing response body: %v", err) + } + if got, want := resp.StatusCode, 200; got != want { + errs <- fmt.Errorf("resp.StatusCode=%d; wantCount %d", got, want) + } + }() + } + + go func() { + wg.Wait() + close(errs) + }() + + for err := range errs { + if err != nil { + t.Fatal(err) + } + } + + for _, viewName := range views { + v := view.Find(viewName) + if v == nil { + t.Errorf("view not found %q", viewName) + continue + } + rows, err := view.RetrieveData(v.Name) + if err != nil { + t.Error(err) + continue + } + if got, want := len(rows), 1; got != want { + t.Errorf("len(%q) = %d; want %d", viewName, got, want) + continue + } + data := rows[0].Data + var count int64 + switch data := data.(type) { + case *view.CountData: + count = data.Value + case *view.DistributionData: + count = data.Count + default: + t.Errorf("Unkown data type: %v", data) + continue + } + if got := count; got != reqCount { + t.Fatalf("%s = %d; want %d", viewName, got, reqCount) + } + } +} + +var noTrace = trace.StartOptions{Sampler: trace.NeverSample()} + +func BenchmarkTransportNoTrace(b *testing.B) { + benchmarkClientServer(b, &ochttp.Transport{StartOptions: noTrace}) +} + +func BenchmarkTransport(b *testing.B) { + benchmarkClientServer(b, &ochttp.Transport{}) +} + +func benchmarkClientServer(b *testing.B, transport *ochttp.Transport) { + b.ReportAllocs() + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + fmt.Fprintf(rw, "Hello world.\n") + })) + defer ts.Close() + transport.StartOptions.Sampler = trace.AlwaysSample() + var client http.Client + client.Transport = transport + b.ResetTimer() + + for i := 0; i < b.N; i++ { + res, err := client.Get(ts.URL) + if err != nil { + b.Fatalf("Get: %v", err) + } + all, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + b.Fatal("ReadAll:", err) + } + body := string(all) + if body != "Hello world.\n" { + b.Fatal("Got body:", body) + } + } +} + +func BenchmarkTransportParallel64NoTrace(b *testing.B) { + benchmarkClientServerParallel(b, 64, &ochttp.Transport{StartOptions: noTrace}) +} + +func BenchmarkTransportParallel64(b *testing.B) { + benchmarkClientServerParallel(b, 64, &ochttp.Transport{}) +} + +func benchmarkClientServerParallel(b *testing.B, parallelism int, transport *ochttp.Transport) { + b.ReportAllocs() + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + fmt.Fprintf(rw, "Hello world.\n") + })) + defer ts.Close() + + var c http.Client + transport.Base = &http.Transport{ + MaxIdleConns: parallelism, + MaxIdleConnsPerHost: parallelism, + } + transport.StartOptions.Sampler = trace.AlwaysSample() + c.Transport = transport + + b.ResetTimer() + + // TODO(ramonza): replace with b.RunParallel (it didn't work when I tried) + + var wg sync.WaitGroup + wg.Add(parallelism) + for i := 0; i < parallelism; i++ { + iterations := b.N / parallelism + if i == 0 { + iterations += b.N % parallelism + } + go func() { + defer wg.Done() + for j := 0; j < iterations; j++ { + res, err := c.Get(ts.URL) + if err != nil { + b.Logf("Get: %v", err) + return + } + all, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + b.Logf("ReadAll: %v", err) + return + } + body := string(all) + if body != "Hello world.\n" { + panic("Got body: " + body) + } + } + }() + } + wg.Wait() +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/doc.go b/vendor/go.opencensus.io/plugin/ochttp/doc.go new file mode 100644 index 0000000000..10e626b16e --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/doc.go @@ -0,0 +1,19 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package ochttp provides OpenCensus instrumentation for net/http package. +// +// For server instrumentation, see Handler. For client-side instrumentation, +// see Transport. +package ochttp // import "go.opencensus.io/plugin/ochttp" diff --git a/vendor/go.opencensus.io/plugin/ochttp/example_test.go b/vendor/go.opencensus.io/plugin/ochttp/example_test.go new file mode 100644 index 0000000000..bb115abb9e --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/example_test.go @@ -0,0 +1,77 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp_test + +import ( + "log" + "net/http" + + "go.opencensus.io/plugin/ochttp" + "go.opencensus.io/plugin/ochttp/propagation/b3" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +func ExampleTransport() { + // import ( + // "go.opencensus.io/plugin/ochttp" + // "go.opencensus.io/stats/view" + // ) + + if err := view.Register( + // Register a few default views. + ochttp.ClientSentBytesDistribution, + ochttp.ClientReceivedBytesDistribution, + ochttp.ClientRoundtripLatencyDistribution, + // Register a custom view. + &view.View{ + Name: "httpclient_latency_by_path", + TagKeys: []tag.Key{ochttp.KeyClientPath}, + Measure: ochttp.ClientRoundtripLatency, + Aggregation: ochttp.DefaultLatencyDistribution, + }, + ); err != nil { + log.Fatal(err) + } + + client := &http.Client{ + Transport: &ochttp.Transport{}, + } + + // Use client to perform requests. + _ = client +} + +var usersHandler http.Handler + +func ExampleHandler() { + // import "go.opencensus.io/plugin/ochttp" + + http.Handle("/users", ochttp.WithRouteTag(usersHandler, "/users")) + + // If no handler is specified, the default mux is used. + log.Fatal(http.ListenAndServe("localhost:8080", &ochttp.Handler{})) +} + +func ExampleHandler_mux() { + // import "go.opencensus.io/plugin/ochttp" + + mux := http.NewServeMux() + mux.Handle("/users", ochttp.WithRouteTag(usersHandler, "/users")) + log.Fatal(http.ListenAndServe("localhost:8080", &ochttp.Handler{ + Handler: mux, + Propagation: &b3.HTTPFormat{}, + })) +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/propagation/b3/b3.go b/vendor/go.opencensus.io/plugin/ochttp/propagation/b3/b3.go new file mode 100644 index 0000000000..f777772ec9 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/propagation/b3/b3.go @@ -0,0 +1,123 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package b3 contains a propagation.HTTPFormat implementation +// for B3 propagation. See https://github.com/openzipkin/b3-propagation +// for more details. +package b3 // import "go.opencensus.io/plugin/ochttp/propagation/b3" + +import ( + "encoding/hex" + "net/http" + + "go.opencensus.io/trace" + "go.opencensus.io/trace/propagation" +) + +// B3 headers that OpenCensus understands. +const ( + TraceIDHeader = "X-B3-TraceId" + SpanIDHeader = "X-B3-SpanId" + SampledHeader = "X-B3-Sampled" +) + +// HTTPFormat implements propagation.HTTPFormat to propagate +// traces in HTTP headers in B3 propagation format. +// HTTPFormat skips the X-B3-ParentId and X-B3-Flags headers +// because there are additional fields not represented in the +// OpenCensus span context. Spans created from the incoming +// header will be the direct children of the client-side span. +// Similarly, reciever of the outgoing spans should use client-side +// span created by OpenCensus as the parent. +type HTTPFormat struct{} + +var _ propagation.HTTPFormat = (*HTTPFormat)(nil) + +// SpanContextFromRequest extracts a B3 span context from incoming requests. +func (f *HTTPFormat) SpanContextFromRequest(req *http.Request) (sc trace.SpanContext, ok bool) { + tid, ok := ParseTraceID(req.Header.Get(TraceIDHeader)) + if !ok { + return trace.SpanContext{}, false + } + sid, ok := ParseSpanID(req.Header.Get(SpanIDHeader)) + if !ok { + return trace.SpanContext{}, false + } + sampled, _ := ParseSampled(req.Header.Get(SampledHeader)) + return trace.SpanContext{ + TraceID: tid, + SpanID: sid, + TraceOptions: sampled, + }, true +} + +// ParseTraceID parses the value of the X-B3-TraceId header. +func ParseTraceID(tid string) (trace.TraceID, bool) { + if tid == "" { + return trace.TraceID{}, false + } + b, err := hex.DecodeString(tid) + if err != nil { + return trace.TraceID{}, false + } + var traceID trace.TraceID + if len(b) <= 8 { + // The lower 64-bits. + start := 8 + (8 - len(b)) + copy(traceID[start:], b) + } else { + start := 16 - len(b) + copy(traceID[start:], b) + } + + return traceID, true +} + +// ParseSpanID parses the value of the X-B3-SpanId or X-B3-ParentSpanId headers. +func ParseSpanID(sid string) (spanID trace.SpanID, ok bool) { + if sid == "" { + return trace.SpanID{}, false + } + b, err := hex.DecodeString(sid) + if err != nil { + return trace.SpanID{}, false + } + start := 8 - len(b) + copy(spanID[start:], b) + return spanID, true +} + +// ParseSampled parses the value of the X-B3-Sampled header. +func ParseSampled(sampled string) (trace.TraceOptions, bool) { + switch sampled { + case "true", "1": + return trace.TraceOptions(1), true + default: + return trace.TraceOptions(0), false + } +} + +// SpanContextToRequest modifies the given request to include B3 headers. +func (f *HTTPFormat) SpanContextToRequest(sc trace.SpanContext, req *http.Request) { + req.Header.Set(TraceIDHeader, hex.EncodeToString(sc.TraceID[:])) + req.Header.Set(SpanIDHeader, hex.EncodeToString(sc.SpanID[:])) + + var sampled string + if sc.IsSampled() { + sampled = "1" + } else { + sampled = "0" + } + req.Header.Set(SampledHeader, sampled) +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/propagation/b3/b3_test.go b/vendor/go.opencensus.io/plugin/ochttp/propagation/b3/b3_test.go new file mode 100644 index 0000000000..4f7b5db86e --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/propagation/b3/b3_test.go @@ -0,0 +1,210 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package b3 + +import ( + "net/http" + "reflect" + "testing" + + "go.opencensus.io/trace" +) + +func TestHTTPFormat_FromRequest(t *testing.T) { + tests := []struct { + name string + makeReq func() *http.Request + wantSc trace.SpanContext + wantOk bool + }{ + { + name: "128-bit trace ID + 64-bit span ID; sampled=1", + makeReq: func() *http.Request { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set(TraceIDHeader, "463ac35c9f6413ad48485a3953bb6124") + req.Header.Set(SpanIDHeader, "0020000000000001") + req.Header.Set(SampledHeader, "1") + return req + }, + wantSc: trace.SpanContext{ + TraceID: trace.TraceID{70, 58, 195, 92, 159, 100, 19, 173, 72, 72, 90, 57, 83, 187, 97, 36}, + SpanID: trace.SpanID{0, 32, 0, 0, 0, 0, 0, 1}, + TraceOptions: trace.TraceOptions(1), + }, + wantOk: true, + }, + { + name: "short trace ID + short span ID; sampled=1", + makeReq: func() *http.Request { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set(TraceIDHeader, "000102") + req.Header.Set(SpanIDHeader, "000102") + req.Header.Set(SampledHeader, "1") + return req + }, + wantSc: trace.SpanContext{ + TraceID: trace.TraceID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2}, + SpanID: trace.SpanID{0, 0, 0, 0, 0, 0, 1, 2}, + TraceOptions: trace.TraceOptions(1), + }, + wantOk: true, + }, + { + name: "64-bit trace ID + 64-bit span ID; sampled=0", + makeReq: func() *http.Request { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set(TraceIDHeader, "0020000000000001") + req.Header.Set(SpanIDHeader, "0020000000000001") + req.Header.Set(SampledHeader, "0") + return req + }, + wantSc: trace.SpanContext{ + TraceID: trace.TraceID{0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 1}, + SpanID: trace.SpanID{0, 32, 0, 0, 0, 0, 0, 1}, + TraceOptions: trace.TraceOptions(0), + }, + wantOk: true, + }, + { + name: "128-bit trace ID + 64-bit span ID; no sampling header", + makeReq: func() *http.Request { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set(TraceIDHeader, "463ac35c9f6413ad48485a3953bb6124") + req.Header.Set(SpanIDHeader, "0020000000000001") + return req + }, + wantSc: trace.SpanContext{ + TraceID: trace.TraceID{70, 58, 195, 92, 159, 100, 19, 173, 72, 72, 90, 57, 83, 187, 97, 36}, + SpanID: trace.SpanID{0, 32, 0, 0, 0, 0, 0, 1}, + TraceOptions: trace.TraceOptions(0), + }, + wantOk: true, + }, + { + name: "invalid trace ID + 64-bit span ID; no sampling header", + makeReq: func() *http.Request { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set(TraceIDHeader, "") + req.Header.Set(SpanIDHeader, "0020000000000001") + return req + }, + wantSc: trace.SpanContext{}, + wantOk: false, + }, + { + name: "128-bit trace ID; invalid span ID; no sampling header", + makeReq: func() *http.Request { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set(TraceIDHeader, "463ac35c9f6413ad48485a3953bb6124") + req.Header.Set(SpanIDHeader, "") + return req + }, + wantSc: trace.SpanContext{}, + wantOk: false, + }, + { + name: "128-bit trace ID + 64-bit span ID; sampled=true", + makeReq: func() *http.Request { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set(TraceIDHeader, "463ac35c9f6413ad48485a3953bb6124") + req.Header.Set(SpanIDHeader, "0020000000000001") + req.Header.Set(SampledHeader, "true") + return req + }, + wantSc: trace.SpanContext{ + TraceID: trace.TraceID{70, 58, 195, 92, 159, 100, 19, 173, 72, 72, 90, 57, 83, 187, 97, 36}, + SpanID: trace.SpanID{0, 32, 0, 0, 0, 0, 0, 1}, + TraceOptions: trace.TraceOptions(1), + }, + wantOk: true, + }, + { + name: "128-bit trace ID + 64-bit span ID; sampled=false", + makeReq: func() *http.Request { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set(TraceIDHeader, "463ac35c9f6413ad48485a3953bb6124") + req.Header.Set(SpanIDHeader, "0020000000000001") + req.Header.Set(SampledHeader, "false") + return req + }, + wantSc: trace.SpanContext{ + TraceID: trace.TraceID{70, 58, 195, 92, 159, 100, 19, 173, 72, 72, 90, 57, 83, 187, 97, 36}, + SpanID: trace.SpanID{0, 32, 0, 0, 0, 0, 0, 1}, + TraceOptions: trace.TraceOptions(0), + }, + wantOk: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &HTTPFormat{} + sc, ok := f.SpanContextFromRequest(tt.makeReq()) + if ok != tt.wantOk { + t.Errorf("HTTPFormat.SpanContextFromRequest() got ok = %v, want %v", ok, tt.wantOk) + } + if !reflect.DeepEqual(sc, tt.wantSc) { + t.Errorf("HTTPFormat.SpanContextFromRequest() got span context = %v, want %v", sc, tt.wantSc) + } + }) + } +} + +func TestHTTPFormat_ToRequest(t *testing.T) { + tests := []struct { + name string + sc trace.SpanContext + wantHeaders map[string]string + }{ + { + name: "valid traceID, header ID, sampled=1", + sc: trace.SpanContext{ + TraceID: trace.TraceID{70, 58, 195, 92, 159, 100, 19, 173, 72, 72, 90, 57, 83, 187, 97, 36}, + SpanID: trace.SpanID{0, 32, 0, 0, 0, 0, 0, 1}, + TraceOptions: trace.TraceOptions(1), + }, + wantHeaders: map[string]string{ + "X-B3-TraceId": "463ac35c9f6413ad48485a3953bb6124", + "X-B3-SpanId": "0020000000000001", + "X-B3-Sampled": "1", + }, + }, + { + name: "valid traceID, header ID, sampled=0", + sc: trace.SpanContext{ + TraceID: trace.TraceID{70, 58, 195, 92, 159, 100, 19, 173, 72, 72, 90, 57, 83, 187, 97, 36}, + SpanID: trace.SpanID{0, 32, 0, 0, 0, 0, 0, 1}, + TraceOptions: trace.TraceOptions(0), + }, + wantHeaders: map[string]string{ + "X-B3-TraceId": "463ac35c9f6413ad48485a3953bb6124", + "X-B3-SpanId": "0020000000000001", + "X-B3-Sampled": "0", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &HTTPFormat{} + req, _ := http.NewRequest("GET", "http://example.com", nil) + f.SpanContextToRequest(tt.sc, req) + + for k, v := range tt.wantHeaders { + if got, want := req.Header.Get(k), v; got != want { + t.Errorf("req.Header.Get(%q) = %q; want %q", k, got, want) + } + } + }) + } +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/propagation/tracecontext/propagation.go b/vendor/go.opencensus.io/plugin/ochttp/propagation/tracecontext/propagation.go new file mode 100644 index 0000000000..65ab1e9966 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/propagation/tracecontext/propagation.go @@ -0,0 +1,187 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tracecontext contains HTTP propagator for TraceContext standard. +// See https://github.com/w3c/distributed-tracing for more information. +package tracecontext // import "go.opencensus.io/plugin/ochttp/propagation/tracecontext" + +import ( + "encoding/hex" + "fmt" + "net/http" + "net/textproto" + "regexp" + "strings" + + "go.opencensus.io/trace" + "go.opencensus.io/trace/propagation" + "go.opencensus.io/trace/tracestate" +) + +const ( + supportedVersion = 0 + maxVersion = 254 + maxTracestateLen = 512 + traceparentHeader = "traceparent" + tracestateHeader = "tracestate" + trimOWSRegexFmt = `^[\x09\x20]*(.*[^\x20\x09])[\x09\x20]*$` +) + +var trimOWSRegExp = regexp.MustCompile(trimOWSRegexFmt) + +var _ propagation.HTTPFormat = (*HTTPFormat)(nil) + +// HTTPFormat implements the TraceContext trace propagation format. +type HTTPFormat struct{} + +// SpanContextFromRequest extracts a span context from incoming requests. +func (f *HTTPFormat) SpanContextFromRequest(req *http.Request) (sc trace.SpanContext, ok bool) { + h, ok := getRequestHeader(req, traceparentHeader, false) + if !ok { + return trace.SpanContext{}, false + } + sections := strings.Split(h, "-") + if len(sections) < 4 { + return trace.SpanContext{}, false + } + + if len(sections[0]) != 2 { + return trace.SpanContext{}, false + } + ver, err := hex.DecodeString(sections[0]) + if err != nil { + return trace.SpanContext{}, false + } + version := int(ver[0]) + if version > maxVersion { + return trace.SpanContext{}, false + } + + if version == 0 && len(sections) != 4 { + return trace.SpanContext{}, false + } + + if len(sections[1]) != 32 { + return trace.SpanContext{}, false + } + tid, err := hex.DecodeString(sections[1]) + if err != nil { + return trace.SpanContext{}, false + } + copy(sc.TraceID[:], tid) + + if len(sections[2]) != 16 { + return trace.SpanContext{}, false + } + sid, err := hex.DecodeString(sections[2]) + if err != nil { + return trace.SpanContext{}, false + } + copy(sc.SpanID[:], sid) + + opts, err := hex.DecodeString(sections[3]) + if err != nil || len(opts) < 1 { + return trace.SpanContext{}, false + } + sc.TraceOptions = trace.TraceOptions(opts[0]) + + // Don't allow all zero trace or span ID. + if sc.TraceID == [16]byte{} || sc.SpanID == [8]byte{} { + return trace.SpanContext{}, false + } + + sc.Tracestate = tracestateFromRequest(req) + return sc, true +} + +// getRequestHeader returns a combined header field according to RFC7230 section 3.2.2. +// If commaSeparated is true, multiple header fields with the same field name using be +// combined using ",". +// If no header was found using the given name, "ok" would be false. +// If more than one headers was found using the given name, while commaSeparated is false, +// "ok" would be false. +func getRequestHeader(req *http.Request, name string, commaSeparated bool) (hdr string, ok bool) { + v := req.Header[textproto.CanonicalMIMEHeaderKey(name)] + switch len(v) { + case 0: + return "", false + case 1: + return v[0], true + default: + return strings.Join(v, ","), commaSeparated + } +} + +// TODO(rghetia): return an empty Tracestate when parsing tracestate header encounters an error. +// Revisit to return additional boolean value to indicate parsing error when following issues +// are resolved. +// https://github.com/w3c/distributed-tracing/issues/172 +// https://github.com/w3c/distributed-tracing/issues/175 +func tracestateFromRequest(req *http.Request) *tracestate.Tracestate { + h, _ := getRequestHeader(req, tracestateHeader, true) + if h == "" { + return nil + } + + var entries []tracestate.Entry + pairs := strings.Split(h, ",") + hdrLenWithoutOWS := len(pairs) - 1 // Number of commas + for _, pair := range pairs { + matches := trimOWSRegExp.FindStringSubmatch(pair) + if matches == nil { + return nil + } + pair = matches[1] + hdrLenWithoutOWS += len(pair) + if hdrLenWithoutOWS > maxTracestateLen { + return nil + } + kv := strings.Split(pair, "=") + if len(kv) != 2 { + return nil + } + entries = append(entries, tracestate.Entry{Key: kv[0], Value: kv[1]}) + } + ts, err := tracestate.New(nil, entries...) + if err != nil { + return nil + } + + return ts +} + +func tracestateToRequest(sc trace.SpanContext, req *http.Request) { + var pairs = make([]string, 0, len(sc.Tracestate.Entries())) + if sc.Tracestate != nil { + for _, entry := range sc.Tracestate.Entries() { + pairs = append(pairs, strings.Join([]string{entry.Key, entry.Value}, "=")) + } + h := strings.Join(pairs, ",") + + if h != "" && len(h) <= maxTracestateLen { + req.Header.Set(tracestateHeader, h) + } + } +} + +// SpanContextToRequest modifies the given request to include traceparent and tracestate headers. +func (f *HTTPFormat) SpanContextToRequest(sc trace.SpanContext, req *http.Request) { + h := fmt.Sprintf("%x-%x-%x-%x", + []byte{supportedVersion}, + sc.TraceID[:], + sc.SpanID[:], + []byte{byte(sc.TraceOptions)}) + req.Header.Set(traceparentHeader, h) + tracestateToRequest(sc, req) +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/propagation/tracecontext/propagation_test.go b/vendor/go.opencensus.io/plugin/ochttp/propagation/tracecontext/propagation_test.go new file mode 100644 index 0000000000..996cfa8838 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/propagation/tracecontext/propagation_test.go @@ -0,0 +1,267 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tracecontext + +import ( + "fmt" + "net/http" + "reflect" + "strings" + "testing" + + "go.opencensus.io/trace" + "go.opencensus.io/trace/tracestate" +) + +var ( + tpHeader = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + traceID = trace.TraceID{75, 249, 47, 53, 119, 179, 77, 166, 163, 206, 146, 157, 14, 14, 71, 54} + spanID = trace.SpanID{0, 240, 103, 170, 11, 169, 2, 183} + traceOpt = trace.TraceOptions(1) + oversizeValue = strings.Repeat("a", maxTracestateLen/2) + oversizeEntry1 = tracestate.Entry{Key: "foo", Value: oversizeValue} + oversizeEntry2 = tracestate.Entry{Key: "hello", Value: oversizeValue} + entry1 = tracestate.Entry{Key: "foo", Value: "bar"} + entry2 = tracestate.Entry{Key: "hello", Value: "world example"} + oversizeTs, _ = tracestate.New(nil, oversizeEntry1, oversizeEntry2) + defaultTs, _ = tracestate.New(nil, nil...) + nonDefaultTs, _ = tracestate.New(nil, entry1, entry2) +) + +func TestHTTPFormat_FromRequest(t *testing.T) { + tests := []struct { + name string + header string + wantSc trace.SpanContext + wantOk bool + }{ + { + name: "future version", + header: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + wantSc: trace.SpanContext{ + TraceID: trace.TraceID{75, 249, 47, 53, 119, 179, 77, 166, 163, 206, 146, 157, 14, 14, 71, 54}, + SpanID: trace.SpanID{0, 240, 103, 170, 11, 169, 2, 183}, + TraceOptions: trace.TraceOptions(1), + }, + wantOk: true, + }, + { + name: "zero trace ID and span ID", + header: "00-00000000000000000000000000000000-0000000000000000-01", + wantSc: trace.SpanContext{}, + wantOk: false, + }, + { + name: "valid header", + header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + wantSc: trace.SpanContext{ + TraceID: trace.TraceID{75, 249, 47, 53, 119, 179, 77, 166, 163, 206, 146, 157, 14, 14, 71, 54}, + SpanID: trace.SpanID{0, 240, 103, 170, 11, 169, 2, 183}, + TraceOptions: trace.TraceOptions(1), + }, + wantOk: true, + }, + { + name: "missing options", + header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7", + wantSc: trace.SpanContext{}, + wantOk: false, + }, + { + name: "empty options", + header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-", + wantSc: trace.SpanContext{}, + wantOk: false, + }, + } + + f := &HTTPFormat{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("traceparent", tt.header) + + gotSc, gotOk := f.SpanContextFromRequest(req) + if !reflect.DeepEqual(gotSc, tt.wantSc) { + t.Errorf("HTTPFormat.FromRequest() gotSc = %v, want %v", gotSc, tt.wantSc) + } + if gotOk != tt.wantOk { + t.Errorf("HTTPFormat.FromRequest() gotOk = %v, want %v", gotOk, tt.wantOk) + } + }) + } +} + +func TestHTTPFormat_ToRequest(t *testing.T) { + tests := []struct { + sc trace.SpanContext + wantHeader string + }{ + { + sc: trace.SpanContext{ + TraceID: trace.TraceID{75, 249, 47, 53, 119, 179, 77, 166, 163, 206, 146, 157, 14, 14, 71, 54}, + SpanID: trace.SpanID{0, 240, 103, 170, 11, 169, 2, 183}, + TraceOptions: trace.TraceOptions(1), + }, + wantHeader: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + }, + } + for _, tt := range tests { + t.Run(tt.wantHeader, func(t *testing.T) { + f := &HTTPFormat{} + req, _ := http.NewRequest("GET", "http://example.com", nil) + f.SpanContextToRequest(tt.sc, req) + + h := req.Header.Get("traceparent") + if got, want := h, tt.wantHeader; got != want { + t.Errorf("HTTPFormat.ToRequest() header = %v, want %v", got, want) + } + }) + } +} + +func TestHTTPFormatTracestate_FromRequest(t *testing.T) { + scWithNonDefaultTracestate := trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceOptions: traceOpt, + Tracestate: nonDefaultTs, + } + + scWithDefaultTracestate := trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceOptions: traceOpt, + Tracestate: defaultTs, + } + + tests := []struct { + name string + tpHeader string + tsHeader string + wantSc trace.SpanContext + wantOk bool + }{ + { + name: "tracestate invalid entries delimiter", + tpHeader: tpHeader, + tsHeader: "foo=bar;hello=world", + wantSc: scWithDefaultTracestate, + wantOk: true, + }, + { + name: "tracestate invalid key-value delimiter", + tpHeader: tpHeader, + tsHeader: "foo=bar,hello-world", + wantSc: scWithDefaultTracestate, + wantOk: true, + }, + { + name: "tracestate invalid value character", + tpHeader: tpHeader, + tsHeader: "foo=bar,hello=world example \u00a0 ", + wantSc: scWithDefaultTracestate, + wantOk: true, + }, + { + name: "tracestate blank key-value", + tpHeader: tpHeader, + tsHeader: "foo=bar, ", + wantSc: scWithDefaultTracestate, + wantOk: true, + }, + { + name: "tracestate oversize header", + tpHeader: tpHeader, + tsHeader: fmt.Sprintf("foo=%s,hello=%s", oversizeValue, oversizeValue), + wantSc: scWithDefaultTracestate, + wantOk: true, + }, + { + name: "tracestate valid", + tpHeader: tpHeader, + tsHeader: "foo=bar , hello=world example", + wantSc: scWithNonDefaultTracestate, + wantOk: true, + }, + } + + f := &HTTPFormat{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("traceparent", tt.tpHeader) + req.Header.Set("tracestate", tt.tsHeader) + + gotSc, gotOk := f.SpanContextFromRequest(req) + if !reflect.DeepEqual(gotSc, tt.wantSc) { + t.Errorf("HTTPFormat.FromRequest() gotTs = %v, want %v", gotSc.Tracestate, tt.wantSc.Tracestate) + } + if gotOk != tt.wantOk { + t.Errorf("HTTPFormat.FromRequest() gotOk = %v, want %v", gotOk, tt.wantOk) + } + }) + } +} + +func TestHTTPFormatTracestate_ToRequest(t *testing.T) { + tests := []struct { + name string + sc trace.SpanContext + wantHeader string + }{ + { + name: "valid span context with default tracestate", + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceOptions: traceOpt, + }, + wantHeader: "", + }, + { + name: "valid span context with non default tracestate", + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceOptions: traceOpt, + Tracestate: nonDefaultTs, + }, + wantHeader: "foo=bar,hello=world example", + }, + { + name: "valid span context with oversize tracestate", + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceOptions: traceOpt, + Tracestate: oversizeTs, + }, + wantHeader: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &HTTPFormat{} + req, _ := http.NewRequest("GET", "http://example.com", nil) + f.SpanContextToRequest(tt.sc, req) + + h := req.Header.Get("tracestate") + if got, want := h, tt.wantHeader; got != want { + t.Errorf("HTTPFormat.ToRequest() tracestate header = %v, want %v", got, want) + } + }) + } +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/propagation_test.go b/vendor/go.opencensus.io/plugin/ochttp/propagation_test.go new file mode 100644 index 0000000000..9ebfd89f4d --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/propagation_test.go @@ -0,0 +1,74 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "go.opencensus.io/plugin/ochttp/propagation/b3" + "go.opencensus.io/plugin/ochttp/propagation/tracecontext" + "go.opencensus.io/trace" + "go.opencensus.io/trace/propagation" +) + +func TestRoundTripAllFormats(t *testing.T) { + // TODO: test combinations of different formats for chains of calls + formats := []propagation.HTTPFormat{ + &b3.HTTPFormat{}, + &tracecontext.HTTPFormat{}, + } + + ctx := context.Background() + ctx, span := trace.StartSpan(ctx, "test", trace.WithSampler(trace.AlwaysSample())) + sc := span.SpanContext() + wantStr := fmt.Sprintf("trace_id=%x, span_id=%x, options=%d", sc.TraceID, sc.SpanID, sc.TraceOptions) + defer span.End() + + for _, format := range formats { + srv := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + sc, ok := format.SpanContextFromRequest(req) + if !ok { + resp.WriteHeader(http.StatusBadRequest) + } + fmt.Fprintf(resp, "trace_id=%x, span_id=%x, options=%d", sc.TraceID, sc.SpanID, sc.TraceOptions) + })) + req, err := http.NewRequest("GET", srv.URL, nil) + if err != nil { + t.Fatal(err) + } + format.SpanContextToRequest(span.SpanContext(), req) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != 200 { + t.Fatal(resp.Status) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + if got, want := string(body), wantStr; got != want { + t.Errorf("%s; want %s", got, want) + } + srv.Close() + } +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/route.go b/vendor/go.opencensus.io/plugin/ochttp/route.go new file mode 100644 index 0000000000..dbe22d5861 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/route.go @@ -0,0 +1,51 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp + +import ( + "net/http" + + "go.opencensus.io/tag" +) + +// WithRouteTag returns an http.Handler that records stats with the +// http_server_route tag set to the given value. +func WithRouteTag(handler http.Handler, route string) http.Handler { + return taggedHandlerFunc(func(w http.ResponseWriter, r *http.Request) []tag.Mutator { + addRoute := []tag.Mutator{tag.Upsert(KeyServerRoute, route)} + ctx, _ := tag.New(r.Context(), addRoute...) + r = r.WithContext(ctx) + handler.ServeHTTP(w, r) + return addRoute + }) +} + +// taggedHandlerFunc is a http.Handler that returns tags describing the +// processing of the request. These tags will be recorded along with the +// measures in this package at the end of the request. +type taggedHandlerFunc func(w http.ResponseWriter, r *http.Request) []tag.Mutator + +func (h taggedHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { + tags := h(w, r) + if a, ok := r.Context().Value(addedTagsKey{}).(*addedTags); ok { + a.t = append(a.t, tags...) + } +} + +type addedTagsKey struct{} + +type addedTags struct { + t []tag.Mutator +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/route_test.go b/vendor/go.opencensus.io/plugin/ochttp/route_test.go new file mode 100644 index 0000000000..a9793eb0bd --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/route_test.go @@ -0,0 +1,80 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/go-cmp/cmp" + "go.opencensus.io/plugin/ochttp" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +func TestWithRouteTag(t *testing.T) { + v := &view.View{ + Name: "request_total", + Measure: ochttp.ServerLatency, + Aggregation: view.Count(), + TagKeys: []tag.Key{ochttp.KeyServerRoute}, + } + view.Register(v) + var e testStatsExporter + view.RegisterExporter(&e) + defer view.UnregisterExporter(&e) + + mux := http.NewServeMux() + handler := ochttp.WithRouteTag(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(204) + }), "/a/") + mux.Handle("/a/", handler) + plugin := ochttp.Handler{Handler: mux} + req, _ := http.NewRequest("GET", "/a/b/c", nil) + rr := httptest.NewRecorder() + plugin.ServeHTTP(rr, req) + if got, want := rr.Code, 204; got != want { + t.Fatalf("Unexpected response, got %d; want %d", got, want) + } + + view.Unregister(v) // trigger exporting + + got := e.rowsForView("request_total") + want := []*view.Row{ + {Data: &view.CountData{Value: 1}, Tags: []tag.Tag{{Key: ochttp.KeyServerRoute, Value: "/a/"}}}, + } + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("Unexpected view data exported, -got, +want: %s", diff) + } +} + +type testStatsExporter struct { + vd []*view.Data +} + +func (t *testStatsExporter) ExportView(d *view.Data) { + t.vd = append(t.vd, d) +} + +func (t *testStatsExporter) rowsForView(name string) []*view.Row { + var rows []*view.Row + for _, d := range t.vd { + if d.View.Name == name { + rows = append(rows, d.Rows...) + } + } + return rows +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/server.go b/vendor/go.opencensus.io/plugin/ochttp/server.go new file mode 100644 index 0000000000..ff72de97a8 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/server.go @@ -0,0 +1,440 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp + +import ( + "context" + "io" + "net/http" + "strconv" + "sync" + "time" + + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" + "go.opencensus.io/trace/propagation" +) + +// Handler is an http.Handler wrapper to instrument your HTTP server with +// OpenCensus. It supports both stats and tracing. +// +// Tracing +// +// This handler is aware of the incoming request's span, reading it from request +// headers as configured using the Propagation field. +// The extracted span can be accessed from the incoming request's +// context. +// +// span := trace.FromContext(r.Context()) +// +// The server span will be automatically ended at the end of ServeHTTP. +type Handler struct { + // Propagation defines how traces are propagated. If unspecified, + // B3 propagation will be used. + Propagation propagation.HTTPFormat + + // Handler is the handler used to handle the incoming request. + Handler http.Handler + + // StartOptions are applied to the span started by this Handler around each + // request. + // + // StartOptions.SpanKind will always be set to trace.SpanKindServer + // for spans started by this transport. + StartOptions trace.StartOptions + + // GetStartOptions allows to set start options per request. If set, + // StartOptions is going to be ignored. + GetStartOptions func(*http.Request) trace.StartOptions + + // IsPublicEndpoint should be set to true for publicly accessible HTTP(S) + // servers. If true, any trace metadata set on the incoming request will + // be added as a linked trace instead of being added as a parent of the + // current trace. + IsPublicEndpoint bool + + // FormatSpanName holds the function to use for generating the span name + // from the information found in the incoming HTTP Request. By default the + // name equals the URL Path. + FormatSpanName func(*http.Request) string +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var tags addedTags + r, traceEnd := h.startTrace(w, r) + defer traceEnd() + w, statsEnd := h.startStats(w, r) + defer statsEnd(&tags) + handler := h.Handler + if handler == nil { + handler = http.DefaultServeMux + } + r = r.WithContext(context.WithValue(r.Context(), addedTagsKey{}, &tags)) + handler.ServeHTTP(w, r) +} + +func (h *Handler) startTrace(w http.ResponseWriter, r *http.Request) (*http.Request, func()) { + if isHealthEndpoint(r.URL.Path) { + return r, func() {} + } + var name string + if h.FormatSpanName == nil { + name = spanNameFromURL(r) + } else { + name = h.FormatSpanName(r) + } + ctx := r.Context() + + startOpts := h.StartOptions + if h.GetStartOptions != nil { + startOpts = h.GetStartOptions(r) + } + + var span *trace.Span + sc, ok := h.extractSpanContext(r) + if ok && !h.IsPublicEndpoint { + ctx, span = trace.StartSpanWithRemoteParent(ctx, name, sc, + trace.WithSampler(startOpts.Sampler), + trace.WithSpanKind(trace.SpanKindServer)) + } else { + ctx, span = trace.StartSpan(ctx, name, + trace.WithSampler(startOpts.Sampler), + trace.WithSpanKind(trace.SpanKindServer), + ) + if ok { + span.AddLink(trace.Link{ + TraceID: sc.TraceID, + SpanID: sc.SpanID, + Type: trace.LinkTypeChild, + Attributes: nil, + }) + } + } + span.AddAttributes(requestAttrs(r)...) + return r.WithContext(ctx), span.End +} + +func (h *Handler) extractSpanContext(r *http.Request) (trace.SpanContext, bool) { + if h.Propagation == nil { + return defaultFormat.SpanContextFromRequest(r) + } + return h.Propagation.SpanContextFromRequest(r) +} + +func (h *Handler) startStats(w http.ResponseWriter, r *http.Request) (http.ResponseWriter, func(tags *addedTags)) { + ctx, _ := tag.New(r.Context(), + tag.Upsert(Host, r.URL.Host), + tag.Upsert(Path, r.URL.Path), + tag.Upsert(Method, r.Method)) + track := &trackingResponseWriter{ + start: time.Now(), + ctx: ctx, + writer: w, + } + if r.Body == nil { + // TODO: Handle cases where ContentLength is not set. + track.reqSize = -1 + } else if r.ContentLength > 0 { + track.reqSize = r.ContentLength + } + stats.Record(ctx, ServerRequestCount.M(1)) + return track.wrappedResponseWriter(), track.end +} + +type trackingResponseWriter struct { + ctx context.Context + reqSize int64 + respSize int64 + start time.Time + statusCode int + statusLine string + endOnce sync.Once + writer http.ResponseWriter +} + +// Compile time assertion for ResponseWriter interface +var _ http.ResponseWriter = (*trackingResponseWriter)(nil) + +var logTagsErrorOnce sync.Once + +func (t *trackingResponseWriter) end(tags *addedTags) { + t.endOnce.Do(func() { + if t.statusCode == 0 { + t.statusCode = 200 + } + + span := trace.FromContext(t.ctx) + span.SetStatus(TraceStatus(t.statusCode, t.statusLine)) + span.AddAttributes(trace.Int64Attribute(StatusCodeAttribute, int64(t.statusCode))) + + m := []stats.Measurement{ + ServerLatency.M(float64(time.Since(t.start)) / float64(time.Millisecond)), + ServerResponseBytes.M(t.respSize), + } + if t.reqSize >= 0 { + m = append(m, ServerRequestBytes.M(t.reqSize)) + } + allTags := make([]tag.Mutator, len(tags.t)+1) + allTags[0] = tag.Upsert(StatusCode, strconv.Itoa(t.statusCode)) + copy(allTags[1:], tags.t) + stats.RecordWithTags(t.ctx, allTags, m...) + }) +} + +func (t *trackingResponseWriter) Header() http.Header { + return t.writer.Header() +} + +func (t *trackingResponseWriter) Write(data []byte) (int, error) { + n, err := t.writer.Write(data) + t.respSize += int64(n) + return n, err +} + +func (t *trackingResponseWriter) WriteHeader(statusCode int) { + t.writer.WriteHeader(statusCode) + t.statusCode = statusCode + t.statusLine = http.StatusText(t.statusCode) +} + +// wrappedResponseWriter returns a wrapped version of the original +// ResponseWriter and only implements the same combination of additional +// interfaces as the original. +// This implementation is based on https://github.com/felixge/httpsnoop. +func (t *trackingResponseWriter) wrappedResponseWriter() http.ResponseWriter { + var ( + hj, i0 = t.writer.(http.Hijacker) + cn, i1 = t.writer.(http.CloseNotifier) + pu, i2 = t.writer.(http.Pusher) + fl, i3 = t.writer.(http.Flusher) + rf, i4 = t.writer.(io.ReaderFrom) + ) + + switch { + case !i0 && !i1 && !i2 && !i3 && !i4: + return struct { + http.ResponseWriter + }{t} + case !i0 && !i1 && !i2 && !i3 && i4: + return struct { + http.ResponseWriter + io.ReaderFrom + }{t, rf} + case !i0 && !i1 && !i2 && i3 && !i4: + return struct { + http.ResponseWriter + http.Flusher + }{t, fl} + case !i0 && !i1 && !i2 && i3 && i4: + return struct { + http.ResponseWriter + http.Flusher + io.ReaderFrom + }{t, fl, rf} + case !i0 && !i1 && i2 && !i3 && !i4: + return struct { + http.ResponseWriter + http.Pusher + }{t, pu} + case !i0 && !i1 && i2 && !i3 && i4: + return struct { + http.ResponseWriter + http.Pusher + io.ReaderFrom + }{t, pu, rf} + case !i0 && !i1 && i2 && i3 && !i4: + return struct { + http.ResponseWriter + http.Pusher + http.Flusher + }{t, pu, fl} + case !i0 && !i1 && i2 && i3 && i4: + return struct { + http.ResponseWriter + http.Pusher + http.Flusher + io.ReaderFrom + }{t, pu, fl, rf} + case !i0 && i1 && !i2 && !i3 && !i4: + return struct { + http.ResponseWriter + http.CloseNotifier + }{t, cn} + case !i0 && i1 && !i2 && !i3 && i4: + return struct { + http.ResponseWriter + http.CloseNotifier + io.ReaderFrom + }{t, cn, rf} + case !i0 && i1 && !i2 && i3 && !i4: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Flusher + }{t, cn, fl} + case !i0 && i1 && !i2 && i3 && i4: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Flusher + io.ReaderFrom + }{t, cn, fl, rf} + case !i0 && i1 && i2 && !i3 && !i4: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Pusher + }{t, cn, pu} + case !i0 && i1 && i2 && !i3 && i4: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Pusher + io.ReaderFrom + }{t, cn, pu, rf} + case !i0 && i1 && i2 && i3 && !i4: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Pusher + http.Flusher + }{t, cn, pu, fl} + case !i0 && i1 && i2 && i3 && i4: + return struct { + http.ResponseWriter + http.CloseNotifier + http.Pusher + http.Flusher + io.ReaderFrom + }{t, cn, pu, fl, rf} + case i0 && !i1 && !i2 && !i3 && !i4: + return struct { + http.ResponseWriter + http.Hijacker + }{t, hj} + case i0 && !i1 && !i2 && !i3 && i4: + return struct { + http.ResponseWriter + http.Hijacker + io.ReaderFrom + }{t, hj, rf} + case i0 && !i1 && !i2 && i3 && !i4: + return struct { + http.ResponseWriter + http.Hijacker + http.Flusher + }{t, hj, fl} + case i0 && !i1 && !i2 && i3 && i4: + return struct { + http.ResponseWriter + http.Hijacker + http.Flusher + io.ReaderFrom + }{t, hj, fl, rf} + case i0 && !i1 && i2 && !i3 && !i4: + return struct { + http.ResponseWriter + http.Hijacker + http.Pusher + }{t, hj, pu} + case i0 && !i1 && i2 && !i3 && i4: + return struct { + http.ResponseWriter + http.Hijacker + http.Pusher + io.ReaderFrom + }{t, hj, pu, rf} + case i0 && !i1 && i2 && i3 && !i4: + return struct { + http.ResponseWriter + http.Hijacker + http.Pusher + http.Flusher + }{t, hj, pu, fl} + case i0 && !i1 && i2 && i3 && i4: + return struct { + http.ResponseWriter + http.Hijacker + http.Pusher + http.Flusher + io.ReaderFrom + }{t, hj, pu, fl, rf} + case i0 && i1 && !i2 && !i3 && !i4: + return struct { + http.ResponseWriter + http.Hijacker + http.CloseNotifier + }{t, hj, cn} + case i0 && i1 && !i2 && !i3 && i4: + return struct { + http.ResponseWriter + http.Hijacker + http.CloseNotifier + io.ReaderFrom + }{t, hj, cn, rf} + case i0 && i1 && !i2 && i3 && !i4: + return struct { + http.ResponseWriter + http.Hijacker + http.CloseNotifier + http.Flusher + }{t, hj, cn, fl} + case i0 && i1 && !i2 && i3 && i4: + return struct { + http.ResponseWriter + http.Hijacker + http.CloseNotifier + http.Flusher + io.ReaderFrom + }{t, hj, cn, fl, rf} + case i0 && i1 && i2 && !i3 && !i4: + return struct { + http.ResponseWriter + http.Hijacker + http.CloseNotifier + http.Pusher + }{t, hj, cn, pu} + case i0 && i1 && i2 && !i3 && i4: + return struct { + http.ResponseWriter + http.Hijacker + http.CloseNotifier + http.Pusher + io.ReaderFrom + }{t, hj, cn, pu, rf} + case i0 && i1 && i2 && i3 && !i4: + return struct { + http.ResponseWriter + http.Hijacker + http.CloseNotifier + http.Pusher + http.Flusher + }{t, hj, cn, pu, fl} + case i0 && i1 && i2 && i3 && i4: + return struct { + http.ResponseWriter + http.Hijacker + http.CloseNotifier + http.Pusher + http.Flusher + io.ReaderFrom + }{t, hj, cn, pu, fl, rf} + default: + return struct { + http.ResponseWriter + }{t} + } +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/server_test.go b/vendor/go.opencensus.io/plugin/ochttp/server_test.go new file mode 100644 index 0000000000..29468cbe47 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/server_test.go @@ -0,0 +1,597 @@ +package ochttp + +import ( + "bufio" + "bytes" + "context" + "crypto/tls" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + "time" + + "golang.org/x/net/http2" + + "go.opencensus.io/stats/view" + "go.opencensus.io/trace" +) + +func httpHandler(statusCode, respSize int) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(statusCode) + body := make([]byte, respSize) + w.Write(body) + }) +} + +func updateMean(mean float64, sample, count int) float64 { + if count == 1 { + return float64(sample) + } + return mean + (float64(sample)-mean)/float64(count) +} + +func TestHandlerStatsCollection(t *testing.T) { + if err := view.Register(DefaultServerViews...); err != nil { + t.Fatalf("Failed to register ochttp.DefaultServerViews error: %v", err) + } + + views := []string{ + "opencensus.io/http/server/request_count", + "opencensus.io/http/server/latency", + "opencensus.io/http/server/request_bytes", + "opencensus.io/http/server/response_bytes", + } + + // TODO: test latency measurements? + tests := []struct { + name, method, target string + count, statusCode, reqSize, respSize int + }{ + {"get 200", "GET", "http://opencensus.io/request/one", 10, 200, 512, 512}, + {"post 503", "POST", "http://opencensus.io/request/two", 5, 503, 1024, 16384}, + {"no body 302", "GET", "http://opencensus.io/request/three", 2, 302, 0, 0}, + } + totalCount, meanReqSize, meanRespSize := 0, 0.0, 0.0 + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + body := bytes.NewBuffer(make([]byte, test.reqSize)) + r := httptest.NewRequest(test.method, test.target, body) + w := httptest.NewRecorder() + mux := http.NewServeMux() + mux.Handle("/request/", httpHandler(test.statusCode, test.respSize)) + h := &Handler{ + Handler: mux, + StartOptions: trace.StartOptions{ + Sampler: trace.NeverSample(), + }, + } + for i := 0; i < test.count; i++ { + h.ServeHTTP(w, r) + totalCount++ + // Distributions do not track sum directly, we must + // mimic their behaviour to avoid rounding failures. + meanReqSize = updateMean(meanReqSize, test.reqSize, totalCount) + meanRespSize = updateMean(meanRespSize, test.respSize, totalCount) + } + }) + } + + for _, viewName := range views { + v := view.Find(viewName) + if v == nil { + t.Errorf("view not found %q", viewName) + continue + } + rows, err := view.RetrieveData(viewName) + if err != nil { + t.Error(err) + continue + } + if got, want := len(rows), 1; got != want { + t.Errorf("len(%q) = %d; want %d", viewName, got, want) + continue + } + data := rows[0].Data + + var count int + var sum float64 + switch data := data.(type) { + case *view.CountData: + count = int(data.Value) + case *view.DistributionData: + count = int(data.Count) + sum = data.Sum() + default: + t.Errorf("Unkown data type: %v", data) + continue + } + + if got, want := count, totalCount; got != want { + t.Fatalf("%s = %d; want %d", viewName, got, want) + } + + // We can only check sum for distribution views. + switch viewName { + case "opencensus.io/http/server/request_bytes": + if got, want := sum, meanReqSize*float64(totalCount); got != want { + t.Fatalf("%s = %g; want %g", viewName, got, want) + } + case "opencensus.io/http/server/response_bytes": + if got, want := sum, meanRespSize*float64(totalCount); got != want { + t.Fatalf("%s = %g; want %g", viewName, got, want) + } + } + } +} + +type testResponseWriterHijacker struct { + httptest.ResponseRecorder +} + +func (trw *testResponseWriterHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return nil, nil, nil +} + +func TestUnitTestHandlerProxiesHijack(t *testing.T) { + tests := []struct { + w http.ResponseWriter + hasHijack bool + }{ + {httptest.NewRecorder(), false}, + {nil, false}, + {new(testResponseWriterHijacker), true}, + } + + for i, tt := range tests { + tw := &trackingResponseWriter{writer: tt.w} + w := tw.wrappedResponseWriter() + _, ttHijacker := w.(http.Hijacker) + if want, have := tt.hasHijack, ttHijacker; want != have { + t.Errorf("#%d Hijack got %t, want %t", i, have, want) + } + } +} + +// Integration test with net/http to ensure that our Handler proxies to its +// response the call to (http.Hijack).Hijacker() and that that successfully +// passes with HTTP/1.1 connections. See Issue #642 +func TestHandlerProxiesHijack_HTTP1(t *testing.T) { + cst := httptest.NewServer(&Handler{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var writeMsg func(string) + defer func() { + err := recover() + writeMsg(fmt.Sprintf("Proto=%s\npanic=%v", r.Proto, err != nil)) + }() + conn, _, _ := w.(http.Hijacker).Hijack() + writeMsg = func(msg string) { + fmt.Fprintf(conn, "%s 200\nContentLength: %d", r.Proto, len(msg)) + fmt.Fprintf(conn, "\r\n\r\n%s", msg) + conn.Close() + } + }), + }) + defer cst.Close() + + testCases := []struct { + name string + tr *http.Transport + want string + }{ + { + name: "http1-transport", + tr: new(http.Transport), + want: "Proto=HTTP/1.1\npanic=false", + }, + { + name: "http2-transport", + tr: func() *http.Transport { + tr := new(http.Transport) + http2.ConfigureTransport(tr) + return tr + }(), + want: "Proto=HTTP/1.1\npanic=false", + }, + } + + for _, tc := range testCases { + c := &http.Client{Transport: &Transport{Base: tc.tr}} + res, err := c.Get(cst.URL) + if err != nil { + t.Errorf("(%s) unexpected error %v", tc.name, err) + continue + } + blob, _ := ioutil.ReadAll(res.Body) + res.Body.Close() + if g, w := string(blob), tc.want; g != w { + t.Errorf("(%s) got = %q; want = %q", tc.name, g, w) + } + } +} + +// Integration test with net/http, x/net/http2 to ensure that our Handler proxies +// to its response the call to (http.Hijack).Hijacker() and that that crashes +// since http.Hijacker and HTTP/2.0 connections are incompatible, but the +// detection is only at runtime and ensure that we can stream and flush to the +// connection even after invoking Hijack(). See Issue #642. +func TestHandlerProxiesHijack_HTTP2(t *testing.T) { + cst := httptest.NewUnstartedServer(&Handler{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if _, ok := w.(http.Hijacker); ok { + conn, _, err := w.(http.Hijacker).Hijack() + if conn != nil { + data := fmt.Sprintf("Surprisingly got the Hijacker() Proto: %s", r.Proto) + fmt.Fprintf(conn, "%s 200\nContent-Length:%d\r\n\r\n%s", r.Proto, len(data), data) + conn.Close() + return + } + + switch { + case err == nil: + fmt.Fprintf(w, "Unexpectedly did not encounter an error!") + default: + fmt.Fprintf(w, "Unexpected error: %v", err) + case strings.Contains(err.(error).Error(), "Hijack"): + // Confirmed HTTP/2.0, let's stream to it + for i := 0; i < 5; i++ { + fmt.Fprintf(w, "%d\n", i) + w.(http.Flusher).Flush() + } + } + } else { + // Confirmed HTTP/2.0, let's stream to it + for i := 0; i < 5; i++ { + fmt.Fprintf(w, "%d\n", i) + w.(http.Flusher).Flush() + } + } + }), + }) + cst.TLS = &tls.Config{NextProtos: []string{"h2"}} + cst.StartTLS() + defer cst.Close() + + if wantPrefix := "https://"; !strings.HasPrefix(cst.URL, wantPrefix) { + t.Fatalf("URL got = %q wantPrefix = %q", cst.URL, wantPrefix) + } + + tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + http2.ConfigureTransport(tr) + c := &http.Client{Transport: tr} + res, err := c.Get(cst.URL) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + blob, _ := ioutil.ReadAll(res.Body) + res.Body.Close() + if g, w := string(blob), "0\n1\n2\n3\n4\n"; g != w { + t.Errorf("got = %q; want = %q", g, w) + } +} + +func TestEnsureTrackingResponseWriterSetsStatusCode(t *testing.T) { + // Ensure that the trackingResponseWriter always sets the spanStatus on ending the span. + // Because we can only examine the Status after exporting, this test roundtrips a + // couple of requests and then later examines the exported spans. + // See Issue #700. + exporter := &spanExporter{cur: make(chan *trace.SpanData, 1)} + trace.RegisterExporter(exporter) + defer trace.UnregisterExporter(exporter) + + tests := []struct { + res *http.Response + want trace.Status + }{ + {res: &http.Response{StatusCode: 200}, want: trace.Status{Code: trace.StatusCodeOK, Message: `OK`}}, + {res: &http.Response{StatusCode: 500}, want: trace.Status{Code: trace.StatusCodeUnknown, Message: `UNKNOWN`}}, + {res: &http.Response{StatusCode: 403}, want: trace.Status{Code: trace.StatusCodePermissionDenied, Message: `PERMISSION_DENIED`}}, + {res: &http.Response{StatusCode: 401}, want: trace.Status{Code: trace.StatusCodeUnauthenticated, Message: `UNAUTHENTICATED`}}, + {res: &http.Response{StatusCode: 429}, want: trace.Status{Code: trace.StatusCodeResourceExhausted, Message: `RESOURCE_EXHAUSTED`}}, + } + + for _, tt := range tests { + t.Run(tt.want.Message, func(t *testing.T) { + ctx := context.Background() + prc, pwc := io.Pipe() + go func() { + pwc.Write([]byte("Foo")) + pwc.Close() + }() + inRes := tt.res + inRes.Body = prc + tr := &traceTransport{ + base: &testResponseTransport{res: inRes}, + formatSpanName: spanNameFromURL, + startOptions: trace.StartOptions{ + Sampler: trace.AlwaysSample(), + }, + } + req, err := http.NewRequest("POST", "https://example.org", bytes.NewReader([]byte("testing"))) + if err != nil { + t.Fatalf("NewRequest error: %v", err) + } + req = req.WithContext(ctx) + res, err := tr.RoundTrip(req) + if err != nil { + t.Fatalf("RoundTrip error: %v", err) + } + _, _ = ioutil.ReadAll(res.Body) + res.Body.Close() + + cur := <-exporter.cur + if got, want := cur.Status, tt.want; got != want { + t.Fatalf("SpanData:\ngot = (%#v)\nwant = (%#v)", got, want) + } + }) + } +} + +type spanExporter struct { + sync.Mutex + cur chan *trace.SpanData +} + +var _ trace.Exporter = (*spanExporter)(nil) + +func (se *spanExporter) ExportSpan(sd *trace.SpanData) { + se.Lock() + se.cur <- sd + se.Unlock() +} + +type testResponseTransport struct { + res *http.Response +} + +var _ http.RoundTripper = (*testResponseTransport)(nil) + +func (rb *testResponseTransport) RoundTrip(*http.Request) (*http.Response, error) { + return rb.res, nil +} + +func TestHandlerImplementsHTTPPusher(t *testing.T) { + cst := setupAndStartServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + pusher, ok := w.(http.Pusher) + if !ok { + w.Write([]byte("false")) + return + } + err := pusher.Push("/static.css", &http.PushOptions{ + Method: "GET", + Header: http.Header{"Accept-Encoding": r.Header["Accept-Encoding"]}, + }) + if err != nil && false { + // TODO: (@odeke-em) consult with Go stdlib for why trying + // to configure even an HTTP/2 server and HTTP/2 transport + // still return http.ErrNotSupported even without using ochttp.Handler. + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.Write([]byte("true")) + }), asHTTP2) + defer cst.Close() + + tests := []struct { + rt http.RoundTripper + wantBody string + }{ + { + rt: h1Transport(), + wantBody: "false", + }, + { + rt: h2Transport(), + wantBody: "true", + }, + { + rt: &Transport{Base: h1Transport()}, + wantBody: "false", + }, + { + rt: &Transport{Base: h2Transport()}, + wantBody: "true", + }, + } + + for i, tt := range tests { + c := &http.Client{Transport: &Transport{Base: tt.rt}} + res, err := c.Get(cst.URL) + if err != nil { + t.Errorf("#%d: Unexpected error %v", i, err) + continue + } + body, _ := ioutil.ReadAll(res.Body) + _ = res.Body.Close() + if g, w := string(body), tt.wantBody; g != w { + t.Errorf("#%d: got = %q; want = %q", i, g, w) + } + } +} + +const ( + isNil = "isNil" + hang = "hang" + ended = "ended" + nonNotifier = "nonNotifier" + + asHTTP1 = false + asHTTP2 = true +) + +func setupAndStartServer(hf func(http.ResponseWriter, *http.Request), isHTTP2 bool) *httptest.Server { + cst := httptest.NewUnstartedServer(&Handler{ + Handler: http.HandlerFunc(hf), + }) + if isHTTP2 { + http2.ConfigureServer(cst.Config, new(http2.Server)) + cst.TLS = cst.Config.TLSConfig + cst.StartTLS() + } else { + cst.Start() + } + + return cst +} + +func insecureTLS() *tls.Config { return &tls.Config{InsecureSkipVerify: true} } +func h1Transport() *http.Transport { return &http.Transport{TLSClientConfig: insecureTLS()} } +func h2Transport() *http.Transport { + tr := &http.Transport{TLSClientConfig: insecureTLS()} + http2.ConfigureTransport(tr) + return tr +} + +type concurrentBuffer struct { + sync.RWMutex + bw *bytes.Buffer +} + +func (cw *concurrentBuffer) Write(b []byte) (int, error) { + cw.Lock() + defer cw.Unlock() + + return cw.bw.Write(b) +} + +func (cw *concurrentBuffer) String() string { + cw.Lock() + defer cw.Unlock() + + return cw.bw.String() +} + +func handleCloseNotify(outLog io.Writer) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cn, ok := w.(http.CloseNotifier) + if !ok { + fmt.Fprintln(outLog, nonNotifier) + return + } + ch := cn.CloseNotify() + if ch == nil { + fmt.Fprintln(outLog, isNil) + return + } + + <-ch + fmt.Fprintln(outLog, ended) + }) +} + +func TestHandlerImplementsHTTPCloseNotify(t *testing.T) { + http1Log := &concurrentBuffer{bw: new(bytes.Buffer)} + http1Server := setupAndStartServer(handleCloseNotify(http1Log), asHTTP1) + http2Log := &concurrentBuffer{bw: new(bytes.Buffer)} + http2Server := setupAndStartServer(handleCloseNotify(http2Log), asHTTP2) + + defer http1Server.Close() + defer http2Server.Close() + + tests := []struct { + url string + want string + }{ + {url: http1Server.URL, want: nonNotifier}, + {url: http2Server.URL, want: ended}, + } + + transports := []struct { + name string + rt http.RoundTripper + }{ + {name: "http2+ochttp", rt: &Transport{Base: h2Transport()}}, + {name: "http1+ochttp", rt: &Transport{Base: h1Transport()}}, + {name: "http1-ochttp", rt: h1Transport()}, + {name: "http2-ochttp", rt: h2Transport()}, + } + + // Each transport invokes one of two server types, either HTTP/1 or HTTP/2 + for _, trc := range transports { + // Try out all the transport combinations + for i, tt := range tests { + req, err := http.NewRequest("GET", tt.url, nil) + if err != nil { + t.Errorf("#%d: Unexpected error making request: %v", i, err) + continue + } + + // Using a timeout to ensure that the request is cancelled and the server + // if its handler implements CloseNotify will see this as the client leaving. + ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond) + defer cancel() + req = req.WithContext(ctx) + + client := &http.Client{Transport: trc.rt} + res, err := client.Do(req) + if err != nil && !strings.Contains(err.Error(), "context deadline exceeded") { + t.Errorf("#%d: %sClient Unexpected error %v", i, trc.name, err) + continue + } + if res != nil && res.Body != nil { + io.CopyN(ioutil.Discard, res.Body, 5) + _ = res.Body.Close() + } + } + } + + // Wait for a couple of milliseconds for the GoAway frames to be properly propagated + <-time.After(150 * time.Millisecond) + + wantHTTP1Log := strings.Repeat("ended\n", len(transports)) + wantHTTP2Log := strings.Repeat("ended\n", len(transports)) + if g, w := http1Log.String(), wantHTTP1Log; g != w { + t.Errorf("HTTP1Log got\n\t%q\nwant\n\t%q", g, w) + } + if g, w := http2Log.String(), wantHTTP2Log; g != w { + t.Errorf("HTTP2Log got\n\t%q\nwant\n\t%q", g, w) + } +} + +func TestIgnoreHealthz(t *testing.T) { + var spans int + + ts := httptest.NewServer(&Handler{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + span := trace.FromContext(r.Context()) + if span != nil { + spans++ + } + fmt.Fprint(w, "ok") + }), + StartOptions: trace.StartOptions{ + Sampler: trace.AlwaysSample(), + }, + }) + defer ts.Close() + + client := &http.Client{} + + for _, path := range []string{"/healthz", "/_ah/health"} { + resp, err := client.Get(ts.URL + path) + if err != nil { + t.Fatalf("Cannot GET %q: %v", path, err) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("Cannot read body for %q: %v", path, err) + } + + if got, want := string(b), "ok"; got != want { + t.Fatalf("Body for %q = %q; want %q", path, got, want) + } + resp.Body.Close() + } + + if spans > 0 { + t.Errorf("Got %v spans; want no spans", spans) + } +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/span_annotating_client_trace.go b/vendor/go.opencensus.io/plugin/ochttp/span_annotating_client_trace.go new file mode 100644 index 0000000000..05c6c56cc7 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/span_annotating_client_trace.go @@ -0,0 +1,169 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp + +import ( + "crypto/tls" + "net/http" + "net/http/httptrace" + "strings" + + "go.opencensus.io/trace" +) + +type spanAnnotator struct { + sp *trace.Span +} + +// TODO: Remove NewSpanAnnotator at the next release. + +// NewSpanAnnotator returns a httptrace.ClientTrace which annotates +// all emitted httptrace events on the provided Span. +// Deprecated: Use NewSpanAnnotatingClientTrace instead +func NewSpanAnnotator(r *http.Request, s *trace.Span) *httptrace.ClientTrace { + return NewSpanAnnotatingClientTrace(r, s) +} + +// NewSpanAnnotatingClientTrace returns a httptrace.ClientTrace which annotates +// all emitted httptrace events on the provided Span. +func NewSpanAnnotatingClientTrace(_ *http.Request, s *trace.Span) *httptrace.ClientTrace { + sa := spanAnnotator{sp: s} + + return &httptrace.ClientTrace{ + GetConn: sa.getConn, + GotConn: sa.gotConn, + PutIdleConn: sa.putIdleConn, + GotFirstResponseByte: sa.gotFirstResponseByte, + Got100Continue: sa.got100Continue, + DNSStart: sa.dnsStart, + DNSDone: sa.dnsDone, + ConnectStart: sa.connectStart, + ConnectDone: sa.connectDone, + TLSHandshakeStart: sa.tlsHandshakeStart, + TLSHandshakeDone: sa.tlsHandshakeDone, + WroteHeaders: sa.wroteHeaders, + Wait100Continue: sa.wait100Continue, + WroteRequest: sa.wroteRequest, + } +} + +func (s spanAnnotator) getConn(hostPort string) { + attrs := []trace.Attribute{ + trace.StringAttribute("httptrace.get_connection.host_port", hostPort), + } + s.sp.Annotate(attrs, "GetConn") +} + +func (s spanAnnotator) gotConn(info httptrace.GotConnInfo) { + attrs := []trace.Attribute{ + trace.BoolAttribute("httptrace.got_connection.reused", info.Reused), + trace.BoolAttribute("httptrace.got_connection.was_idle", info.WasIdle), + } + if info.WasIdle { + attrs = append(attrs, + trace.StringAttribute("httptrace.got_connection.idle_time", info.IdleTime.String())) + } + s.sp.Annotate(attrs, "GotConn") +} + +// PutIdleConn implements a httptrace.ClientTrace hook +func (s spanAnnotator) putIdleConn(err error) { + var attrs []trace.Attribute + if err != nil { + attrs = append(attrs, + trace.StringAttribute("httptrace.put_idle_connection.error", err.Error())) + } + s.sp.Annotate(attrs, "PutIdleConn") +} + +func (s spanAnnotator) gotFirstResponseByte() { + s.sp.Annotate(nil, "GotFirstResponseByte") +} + +func (s spanAnnotator) got100Continue() { + s.sp.Annotate(nil, "Got100Continue") +} + +func (s spanAnnotator) dnsStart(info httptrace.DNSStartInfo) { + attrs := []trace.Attribute{ + trace.StringAttribute("httptrace.dns_start.host", info.Host), + } + s.sp.Annotate(attrs, "DNSStart") +} + +func (s spanAnnotator) dnsDone(info httptrace.DNSDoneInfo) { + var addrs []string + for _, addr := range info.Addrs { + addrs = append(addrs, addr.String()) + } + attrs := []trace.Attribute{ + trace.StringAttribute("httptrace.dns_done.addrs", strings.Join(addrs, " , ")), + } + if info.Err != nil { + attrs = append(attrs, + trace.StringAttribute("httptrace.dns_done.error", info.Err.Error())) + } + s.sp.Annotate(attrs, "DNSDone") +} + +func (s spanAnnotator) connectStart(network, addr string) { + attrs := []trace.Attribute{ + trace.StringAttribute("httptrace.connect_start.network", network), + trace.StringAttribute("httptrace.connect_start.addr", addr), + } + s.sp.Annotate(attrs, "ConnectStart") +} + +func (s spanAnnotator) connectDone(network, addr string, err error) { + attrs := []trace.Attribute{ + trace.StringAttribute("httptrace.connect_done.network", network), + trace.StringAttribute("httptrace.connect_done.addr", addr), + } + if err != nil { + attrs = append(attrs, + trace.StringAttribute("httptrace.connect_done.error", err.Error())) + } + s.sp.Annotate(attrs, "ConnectDone") +} + +func (s spanAnnotator) tlsHandshakeStart() { + s.sp.Annotate(nil, "TLSHandshakeStart") +} + +func (s spanAnnotator) tlsHandshakeDone(_ tls.ConnectionState, err error) { + var attrs []trace.Attribute + if err != nil { + attrs = append(attrs, + trace.StringAttribute("httptrace.tls_handshake_done.error", err.Error())) + } + s.sp.Annotate(attrs, "TLSHandshakeDone") +} + +func (s spanAnnotator) wroteHeaders() { + s.sp.Annotate(nil, "WroteHeaders") +} + +func (s spanAnnotator) wait100Continue() { + s.sp.Annotate(nil, "Wait100Continue") +} + +func (s spanAnnotator) wroteRequest(info httptrace.WroteRequestInfo) { + var attrs []trace.Attribute + if info.Err != nil { + attrs = append(attrs, + trace.StringAttribute("httptrace.wrote_request.error", info.Err.Error())) + } + s.sp.Annotate(attrs, "WroteRequest") +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/span_annotating_client_trace_test.go b/vendor/go.opencensus.io/plugin/ochttp/span_annotating_client_trace_test.go new file mode 100644 index 0000000000..3336d7ec96 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/span_annotating_client_trace_test.go @@ -0,0 +1,109 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp_test + +import ( + "errors" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + + "go.opencensus.io/plugin/ochttp" + "go.opencensus.io/trace" +) + +func TestSpanAnnotatingClientTrace(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + resp.Write([]byte("Hello, world!")) + })) + defer server.Close() + + recorder := &testExporter{} + + trace.RegisterExporter(recorder) + + tr := ochttp.Transport{ + NewClientTrace: ochttp.NewSpanAnnotatingClientTrace, + StartOptions: trace.StartOptions{ + Sampler: trace.AlwaysSample(), + }, + } + + req, err := http.NewRequest("POST", server.URL, strings.NewReader("req-body")) + if err != nil { + t.Errorf("error creating request: %v", err) + } + + resp, err := tr.RoundTrip(req) + if err != nil { + t.Errorf("response error: %v", err) + } + if err := resp.Body.Close(); err != nil { + t.Errorf("error closing response body: %v", err) + } + if got, want := resp.StatusCode, 200; got != want { + t.Errorf("resp.StatusCode=%d; want=%d", got, want) + } + + if got, want := len(recorder.spans), 1; got != want { + t.Fatalf("span count=%d; want=%d", got, want) + } + + var annotations []string + for _, annotation := range recorder.spans[0].Annotations { + annotations = append(annotations, annotation.Message) + } + + required := []string{ + "GetConn", "GotConn", "GotFirstResponseByte", "ConnectStart", + "ConnectDone", "WroteHeaders", "WroteRequest", + } + + if errs := requiredAnnotations(required, annotations); len(errs) > 0 { + for _, err := range errs { + t.Error(err) + } + } + +} + +type testExporter struct { + mu sync.Mutex + spans []*trace.SpanData +} + +func (t *testExporter) ExportSpan(s *trace.SpanData) { + t.mu.Lock() + t.spans = append(t.spans, s) + t.mu.Unlock() +} + +func requiredAnnotations(required []string, list []string) []error { + var errs []error + for _, item := range required { + var found bool + for _, v := range list { + if v == item { + found = true + } + } + if !found { + errs = append(errs, errors.New("missing expected annotation: "+item)) + } + } + return errs +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/stats.go b/vendor/go.opencensus.io/plugin/ochttp/stats.go new file mode 100644 index 0000000000..46dcc8e57e --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/stats.go @@ -0,0 +1,265 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp + +import ( + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +// The following client HTTP measures are supported for use in custom views. +var ( + // Deprecated: Use a Count aggregation over one of the other client measures to achieve the same effect. + ClientRequestCount = stats.Int64("opencensus.io/http/client/request_count", "Number of HTTP requests started", stats.UnitDimensionless) + // Deprecated: Use ClientSentBytes. + ClientRequestBytes = stats.Int64("opencensus.io/http/client/request_bytes", "HTTP request body size if set as ContentLength (uncompressed)", stats.UnitBytes) + // Deprecated: Use ClientReceivedBytes. + ClientResponseBytes = stats.Int64("opencensus.io/http/client/response_bytes", "HTTP response body size (uncompressed)", stats.UnitBytes) + // Deprecated: Use ClientRoundtripLatency. + ClientLatency = stats.Float64("opencensus.io/http/client/latency", "End-to-end latency", stats.UnitMilliseconds) +) + +// Client measures supported for use in custom views. +var ( + ClientSentBytes = stats.Int64( + "opencensus.io/http/client/sent_bytes", + "Total bytes sent in request body (not including headers)", + stats.UnitBytes, + ) + ClientReceivedBytes = stats.Int64( + "opencensus.io/http/client/received_bytes", + "Total bytes received in response bodies (not including headers but including error responses with bodies)", + stats.UnitBytes, + ) + ClientRoundtripLatency = stats.Float64( + "opencensus.io/http/client/roundtrip_latency", + "Time between first byte of request headers sent to last byte of response received, or terminal error", + stats.UnitMilliseconds, + ) +) + +// The following server HTTP measures are supported for use in custom views: +var ( + ServerRequestCount = stats.Int64("opencensus.io/http/server/request_count", "Number of HTTP requests started", stats.UnitDimensionless) + ServerRequestBytes = stats.Int64("opencensus.io/http/server/request_bytes", "HTTP request body size if set as ContentLength (uncompressed)", stats.UnitBytes) + ServerResponseBytes = stats.Int64("opencensus.io/http/server/response_bytes", "HTTP response body size (uncompressed)", stats.UnitBytes) + ServerLatency = stats.Float64("opencensus.io/http/server/latency", "End-to-end latency", stats.UnitMilliseconds) +) + +// The following tags are applied to stats recorded by this package. Host, Path +// and Method are applied to all measures. StatusCode is not applied to +// ClientRequestCount or ServerRequestCount, since it is recorded before the status is known. +var ( + // Host is the value of the HTTP Host header. + // + // The value of this tag can be controlled by the HTTP client, so you need + // to watch out for potentially generating high-cardinality labels in your + // metrics backend if you use this tag in views. + Host, _ = tag.NewKey("http.host") + + // StatusCode is the numeric HTTP response status code, + // or "error" if a transport error occurred and no status code was read. + StatusCode, _ = tag.NewKey("http.status") + + // Path is the URL path (not including query string) in the request. + // + // The value of this tag can be controlled by the HTTP client, so you need + // to watch out for potentially generating high-cardinality labels in your + // metrics backend if you use this tag in views. + Path, _ = tag.NewKey("http.path") + + // Method is the HTTP method of the request, capitalized (GET, POST, etc.). + Method, _ = tag.NewKey("http.method") + + // KeyServerRoute is a low cardinality string representing the logical + // handler of the request. This is usually the pattern registered on the a + // ServeMux (or similar string). + KeyServerRoute, _ = tag.NewKey("http_server_route") +) + +// Client tag keys. +var ( + // KeyClientMethod is the HTTP method, capitalized (i.e. GET, POST, PUT, DELETE, etc.). + KeyClientMethod, _ = tag.NewKey("http_client_method") + // KeyClientPath is the URL path (not including query string). + KeyClientPath, _ = tag.NewKey("http_client_path") + // KeyClientStatus is the HTTP status code as an integer (e.g. 200, 404, 500.), or "error" if no response status line was received. + KeyClientStatus, _ = tag.NewKey("http_client_status") + // KeyClientHost is the value of the request Host header. + KeyClientHost, _ = tag.NewKey("http_client_host") +) + +// Default distributions used by views in this package. +var ( + DefaultSizeDistribution = view.Distribution(0, 1024, 2048, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, 268435456, 1073741824, 4294967296) + DefaultLatencyDistribution = view.Distribution(0, 1, 2, 3, 4, 5, 6, 8, 10, 13, 16, 20, 25, 30, 40, 50, 65, 80, 100, 130, 160, 200, 250, 300, 400, 500, 650, 800, 1000, 2000, 5000, 10000, 20000, 50000, 100000) +) + +// Package ochttp provides some convenience views. +// You still need to register these views for data to actually be collected. +var ( + ClientSentBytesDistribution = &view.View{ + Name: "opencensus.io/http/client/sent_bytes", + Measure: ClientSentBytes, + Aggregation: DefaultSizeDistribution, + Description: "Total bytes sent in request body (not including headers), by HTTP method and response status", + TagKeys: []tag.Key{KeyClientMethod, KeyClientStatus}, + } + + ClientReceivedBytesDistribution = &view.View{ + Name: "opencensus.io/http/client/received_bytes", + Measure: ClientReceivedBytes, + Aggregation: DefaultSizeDistribution, + Description: "Total bytes received in response bodies (not including headers but including error responses with bodies), by HTTP method and response status", + TagKeys: []tag.Key{KeyClientMethod, KeyClientStatus}, + } + + ClientRoundtripLatencyDistribution = &view.View{ + Name: "opencensus.io/http/client/roundtrip_latency", + Measure: ClientRoundtripLatency, + Aggregation: DefaultLatencyDistribution, + Description: "End-to-end latency, by HTTP method and response status", + TagKeys: []tag.Key{KeyClientMethod, KeyClientStatus}, + } + + ClientCompletedCount = &view.View{ + Name: "opencensus.io/http/client/completed_count", + Measure: ClientRoundtripLatency, + Aggregation: view.Count(), + Description: "Count of completed requests, by HTTP method and response status", + TagKeys: []tag.Key{KeyClientMethod, KeyClientStatus}, + } +) + +var ( + // Deprecated: No direct replacement, but see ClientCompletedCount. + ClientRequestCountView = &view.View{ + Name: "opencensus.io/http/client/request_count", + Description: "Count of HTTP requests started", + Measure: ClientRequestCount, + Aggregation: view.Count(), + } + + // Deprecated: Use ClientSentBytesDistribution. + ClientRequestBytesView = &view.View{ + Name: "opencensus.io/http/client/request_bytes", + Description: "Size distribution of HTTP request body", + Measure: ClientSentBytes, + Aggregation: DefaultSizeDistribution, + } + + // Deprecated: Use ClientReceivedBytesDistribution. + ClientResponseBytesView = &view.View{ + Name: "opencensus.io/http/client/response_bytes", + Description: "Size distribution of HTTP response body", + Measure: ClientReceivedBytes, + Aggregation: DefaultSizeDistribution, + } + + // Deprecated: Use ClientRoundtripLatencyDistribution. + ClientLatencyView = &view.View{ + Name: "opencensus.io/http/client/latency", + Description: "Latency distribution of HTTP requests", + Measure: ClientRoundtripLatency, + Aggregation: DefaultLatencyDistribution, + } + + // Deprecated: Use ClientCompletedCount. + ClientRequestCountByMethod = &view.View{ + Name: "opencensus.io/http/client/request_count_by_method", + Description: "Client request count by HTTP method", + TagKeys: []tag.Key{Method}, + Measure: ClientSentBytes, + Aggregation: view.Count(), + } + + // Deprecated: Use ClientCompletedCount. + ClientResponseCountByStatusCode = &view.View{ + Name: "opencensus.io/http/client/response_count_by_status_code", + Description: "Client response count by status code", + TagKeys: []tag.Key{StatusCode}, + Measure: ClientRoundtripLatency, + Aggregation: view.Count(), + } +) + +var ( + ServerRequestCountView = &view.View{ + Name: "opencensus.io/http/server/request_count", + Description: "Count of HTTP requests started", + Measure: ServerRequestCount, + Aggregation: view.Count(), + } + + ServerRequestBytesView = &view.View{ + Name: "opencensus.io/http/server/request_bytes", + Description: "Size distribution of HTTP request body", + Measure: ServerRequestBytes, + Aggregation: DefaultSizeDistribution, + } + + ServerResponseBytesView = &view.View{ + Name: "opencensus.io/http/server/response_bytes", + Description: "Size distribution of HTTP response body", + Measure: ServerResponseBytes, + Aggregation: DefaultSizeDistribution, + } + + ServerLatencyView = &view.View{ + Name: "opencensus.io/http/server/latency", + Description: "Latency distribution of HTTP requests", + Measure: ServerLatency, + Aggregation: DefaultLatencyDistribution, + } + + ServerRequestCountByMethod = &view.View{ + Name: "opencensus.io/http/server/request_count_by_method", + Description: "Server request count by HTTP method", + TagKeys: []tag.Key{Method}, + Measure: ServerRequestCount, + Aggregation: view.Count(), + } + + ServerResponseCountByStatusCode = &view.View{ + Name: "opencensus.io/http/server/response_count_by_status_code", + Description: "Server response count by status code", + TagKeys: []tag.Key{StatusCode}, + Measure: ServerLatency, + Aggregation: view.Count(), + } +) + +// DefaultClientViews are the default client views provided by this package. +// Deprecated: No replacement. Register the views you would like individually. +var DefaultClientViews = []*view.View{ + ClientRequestCountView, + ClientRequestBytesView, + ClientResponseBytesView, + ClientLatencyView, + ClientRequestCountByMethod, + ClientResponseCountByStatusCode, +} + +// DefaultServerViews are the default server views provided by this package. +// Deprecated: No replacement. Register the views you would like individually. +var DefaultServerViews = []*view.View{ + ServerRequestCountView, + ServerRequestBytesView, + ServerResponseBytesView, + ServerLatencyView, + ServerRequestCountByMethod, + ServerResponseCountByStatusCode, +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/stats_test.go b/vendor/go.opencensus.io/plugin/ochttp/stats_test.go new file mode 100644 index 0000000000..ce3621bf3b --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/stats_test.go @@ -0,0 +1,88 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp + +import ( + "reflect" + "strings" + "testing" + + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +func TestClientViews(t *testing.T) { + for _, v := range []*view.View{ + ClientSentBytesDistribution, + ClientReceivedBytesDistribution, + ClientRoundtripLatencyDistribution, + ClientCompletedCount, + } { + + if v.Measure == nil { + t.Fatalf("nil measure: %v", v) + } + if m := v.Measure.Name(); !strings.HasPrefix(m, "opencensus.io/http/client/") { + t.Errorf("Unexpected measure name prefix: %v", v) + } + if v.Name == "" { + t.Errorf("Empty name: %v", v) + } + if !strings.HasPrefix(v.Name, "opencensus.io/http/client/") { + t.Errorf("Unexpected prefix: %s", v.Name) + } + if v.Description == "" { + t.Errorf("Empty description: %s", v.Name) + } + if !reflect.DeepEqual(v.TagKeys, []tag.Key{KeyClientMethod, KeyClientStatus}) { + t.Errorf("Unexpected tags for client view %s: %v", v.Name, v.TagKeys) + } + if strings.HasSuffix(v.Description, ".") { + t.Errorf("View description should not end with a period: %s", v.Name) + } + } +} + +func TestClientTagKeys(t *testing.T) { + for _, k := range []tag.Key{ + KeyClientStatus, + KeyClientMethod, + KeyClientHost, + KeyClientPath, + } { + if !strings.HasPrefix(k.Name(), "http_client_") { + t.Errorf("Unexpected prefix: %s", k.Name()) + } + } +} + +func TestClientMeasures(t *testing.T) { + for _, m := range []stats.Measure{ + ClientSentBytes, + ClientReceivedBytes, + ClientRoundtripLatency, + } { + if !strings.HasPrefix(m.Name(), "opencensus.io/http/client/") { + t.Errorf("Unexpected prefix: %v", m) + } + if strings.HasSuffix(m.Description(), ".") { + t.Errorf("View description should not end with a period: %s", m.Name()) + } + if len(m.Unit()) == 0 { + t.Errorf("No unit: %s", m.Name()) + } + } +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/trace.go b/vendor/go.opencensus.io/plugin/ochttp/trace.go new file mode 100644 index 0000000000..819a2d5ff9 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/trace.go @@ -0,0 +1,228 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp + +import ( + "io" + "net/http" + "net/http/httptrace" + + "go.opencensus.io/plugin/ochttp/propagation/b3" + "go.opencensus.io/trace" + "go.opencensus.io/trace/propagation" +) + +// TODO(jbd): Add godoc examples. + +var defaultFormat propagation.HTTPFormat = &b3.HTTPFormat{} + +// Attributes recorded on the span for the requests. +// Only trace exporters will need them. +const ( + HostAttribute = "http.host" + MethodAttribute = "http.method" + PathAttribute = "http.path" + UserAgentAttribute = "http.user_agent" + StatusCodeAttribute = "http.status_code" +) + +type traceTransport struct { + base http.RoundTripper + startOptions trace.StartOptions + format propagation.HTTPFormat + formatSpanName func(*http.Request) string + newClientTrace func(*http.Request, *trace.Span) *httptrace.ClientTrace +} + +// TODO(jbd): Add message events for request and response size. + +// RoundTrip creates a trace.Span and inserts it into the outgoing request's headers. +// The created span can follow a parent span, if a parent is presented in +// the request's context. +func (t *traceTransport) RoundTrip(req *http.Request) (*http.Response, error) { + name := t.formatSpanName(req) + // TODO(jbd): Discuss whether we want to prefix + // outgoing requests with Sent. + ctx, span := trace.StartSpan(req.Context(), name, + trace.WithSampler(t.startOptions.Sampler), + trace.WithSpanKind(trace.SpanKindClient)) + + if t.newClientTrace != nil { + req = req.WithContext(httptrace.WithClientTrace(ctx, t.newClientTrace(req, span))) + } else { + req = req.WithContext(ctx) + } + + if t.format != nil { + // SpanContextToRequest will modify its Request argument, which is + // contrary to the contract for http.RoundTripper, so we need to + // pass it a copy of the Request. + // However, the Request struct itself was already copied by + // the WithContext calls above and so we just need to copy the header. + header := make(http.Header) + for k, v := range req.Header { + header[k] = v + } + req.Header = header + t.format.SpanContextToRequest(span.SpanContext(), req) + } + + span.AddAttributes(requestAttrs(req)...) + resp, err := t.base.RoundTrip(req) + if err != nil { + span.SetStatus(trace.Status{Code: trace.StatusCodeUnknown, Message: err.Error()}) + span.End() + return resp, err + } + + span.AddAttributes(responseAttrs(resp)...) + span.SetStatus(TraceStatus(resp.StatusCode, resp.Status)) + + // span.End() will be invoked after + // a read from resp.Body returns io.EOF or when + // resp.Body.Close() is invoked. + resp.Body = &bodyTracker{rc: resp.Body, span: span} + return resp, err +} + +// bodyTracker wraps a response.Body and invokes +// trace.EndSpan on encountering io.EOF on reading +// the body of the original response. +type bodyTracker struct { + rc io.ReadCloser + span *trace.Span +} + +var _ io.ReadCloser = (*bodyTracker)(nil) + +func (bt *bodyTracker) Read(b []byte) (int, error) { + n, err := bt.rc.Read(b) + + switch err { + case nil: + return n, nil + case io.EOF: + bt.span.End() + default: + // For all other errors, set the span status + bt.span.SetStatus(trace.Status{ + // Code 2 is the error code for Internal server error. + Code: 2, + Message: err.Error(), + }) + } + return n, err +} + +func (bt *bodyTracker) Close() error { + // Invoking endSpan on Close will help catch the cases + // in which a read returned a non-nil error, we set the + // span status but didn't end the span. + bt.span.End() + return bt.rc.Close() +} + +// CancelRequest cancels an in-flight request by closing its connection. +func (t *traceTransport) CancelRequest(req *http.Request) { + type canceler interface { + CancelRequest(*http.Request) + } + if cr, ok := t.base.(canceler); ok { + cr.CancelRequest(req) + } +} + +func spanNameFromURL(req *http.Request) string { + return req.URL.Path +} + +func requestAttrs(r *http.Request) []trace.Attribute { + return []trace.Attribute{ + trace.StringAttribute(PathAttribute, r.URL.Path), + trace.StringAttribute(HostAttribute, r.URL.Host), + trace.StringAttribute(MethodAttribute, r.Method), + trace.StringAttribute(UserAgentAttribute, r.UserAgent()), + } +} + +func responseAttrs(resp *http.Response) []trace.Attribute { + return []trace.Attribute{ + trace.Int64Attribute(StatusCodeAttribute, int64(resp.StatusCode)), + } +} + +// TraceStatus is a utility to convert the HTTP status code to a trace.Status that +// represents the outcome as closely as possible. +func TraceStatus(httpStatusCode int, statusLine string) trace.Status { + var code int32 + if httpStatusCode < 200 || httpStatusCode >= 400 { + code = trace.StatusCodeUnknown + } + switch httpStatusCode { + case 499: + code = trace.StatusCodeCancelled + case http.StatusBadRequest: + code = trace.StatusCodeInvalidArgument + case http.StatusGatewayTimeout: + code = trace.StatusCodeDeadlineExceeded + case http.StatusNotFound: + code = trace.StatusCodeNotFound + case http.StatusForbidden: + code = trace.StatusCodePermissionDenied + case http.StatusUnauthorized: // 401 is actually unauthenticated. + code = trace.StatusCodeUnauthenticated + case http.StatusTooManyRequests: + code = trace.StatusCodeResourceExhausted + case http.StatusNotImplemented: + code = trace.StatusCodeUnimplemented + case http.StatusServiceUnavailable: + code = trace.StatusCodeUnavailable + case http.StatusOK: + code = trace.StatusCodeOK + } + return trace.Status{Code: code, Message: codeToStr[code]} +} + +var codeToStr = map[int32]string{ + trace.StatusCodeOK: `OK`, + trace.StatusCodeCancelled: `CANCELLED`, + trace.StatusCodeUnknown: `UNKNOWN`, + trace.StatusCodeInvalidArgument: `INVALID_ARGUMENT`, + trace.StatusCodeDeadlineExceeded: `DEADLINE_EXCEEDED`, + trace.StatusCodeNotFound: `NOT_FOUND`, + trace.StatusCodeAlreadyExists: `ALREADY_EXISTS`, + trace.StatusCodePermissionDenied: `PERMISSION_DENIED`, + trace.StatusCodeResourceExhausted: `RESOURCE_EXHAUSTED`, + trace.StatusCodeFailedPrecondition: `FAILED_PRECONDITION`, + trace.StatusCodeAborted: `ABORTED`, + trace.StatusCodeOutOfRange: `OUT_OF_RANGE`, + trace.StatusCodeUnimplemented: `UNIMPLEMENTED`, + trace.StatusCodeInternal: `INTERNAL`, + trace.StatusCodeUnavailable: `UNAVAILABLE`, + trace.StatusCodeDataLoss: `DATA_LOSS`, + trace.StatusCodeUnauthenticated: `UNAUTHENTICATED`, +} + +func isHealthEndpoint(path string) bool { + // Health checking is pretty frequent and + // traces collected for health endpoints + // can be extremely noisy and expensive. + // Disable canonical health checking endpoints + // like /healthz and /_ah/health for now. + if path == "/healthz" || path == "/_ah/health" { + return true + } + return false +} diff --git a/vendor/go.opencensus.io/plugin/ochttp/trace_test.go b/vendor/go.opencensus.io/plugin/ochttp/trace_test.go new file mode 100644 index 0000000000..ea9b77c057 --- /dev/null +++ b/vendor/go.opencensus.io/plugin/ochttp/trace_test.go @@ -0,0 +1,543 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ochttp + +import ( + "bytes" + "context" + "encoding/hex" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" + "time" + + "go.opencensus.io/plugin/ochttp/propagation/b3" + "go.opencensus.io/plugin/ochttp/propagation/tracecontext" + "go.opencensus.io/trace" +) + +type testExporter struct { + spans []*trace.SpanData +} + +func (t *testExporter) ExportSpan(s *trace.SpanData) { + t.spans = append(t.spans, s) +} + +type testTransport struct { + ch chan *http.Request +} + +func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) { + t.ch <- req + return nil, errors.New("noop") +} + +type testPropagator struct{} + +func (t testPropagator) SpanContextFromRequest(req *http.Request) (sc trace.SpanContext, ok bool) { + header := req.Header.Get("trace") + buf, err := hex.DecodeString(header) + if err != nil { + log.Fatalf("Cannot decode trace header: %q", header) + } + r := bytes.NewReader(buf) + r.Read(sc.TraceID[:]) + r.Read(sc.SpanID[:]) + opts, err := r.ReadByte() + if err != nil { + log.Fatalf("Cannot read trace options from trace header: %q", header) + } + sc.TraceOptions = trace.TraceOptions(opts) + return sc, true +} + +func (t testPropagator) SpanContextToRequest(sc trace.SpanContext, req *http.Request) { + var buf bytes.Buffer + buf.Write(sc.TraceID[:]) + buf.Write(sc.SpanID[:]) + buf.WriteByte(byte(sc.TraceOptions)) + req.Header.Set("trace", hex.EncodeToString(buf.Bytes())) +} + +func TestTransport_RoundTrip_Race(t *testing.T) { + // This tests that we don't modify the request in accordance with the + // specification for http.RoundTripper. + // We attempt to trigger a race by reading the request from a separate + // goroutine. If the request is modified by Transport, this should trigger + // the race detector. + + transport := &testTransport{ch: make(chan *http.Request, 1)} + rt := &Transport{ + Propagation: &testPropagator{}, + Base: transport, + } + req, _ := http.NewRequest("GET", "http://foo.com", nil) + go func() { + fmt.Println(*req) + }() + rt.RoundTrip(req) + _ = <-transport.ch +} + +func TestTransport_RoundTrip(t *testing.T) { + ctx := context.Background() + ctx, parent := trace.StartSpan(ctx, "parent") + tests := []struct { + name string + parent *trace.Span + }{ + { + name: "no parent", + parent: nil, + }, + { + name: "parent", + parent: parent, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + transport := &testTransport{ch: make(chan *http.Request, 1)} + + rt := &Transport{ + Propagation: &testPropagator{}, + Base: transport, + } + + req, _ := http.NewRequest("GET", "http://foo.com", nil) + if tt.parent != nil { + req = req.WithContext(trace.NewContext(req.Context(), tt.parent)) + } + rt.RoundTrip(req) + + req = <-transport.ch + span := trace.FromContext(req.Context()) + + if header := req.Header.Get("trace"); header == "" { + t.Fatalf("Trace header = empty; want valid trace header") + } + if span == nil { + t.Fatalf("Got no spans in req context; want one") + } + if tt.parent != nil { + if got, want := span.SpanContext().TraceID, tt.parent.SpanContext().TraceID; got != want { + t.Errorf("span.SpanContext().TraceID=%v; want %v", got, want) + } + } + }) + } +} + +func TestHandler(t *testing.T) { + traceID := [16]byte{16, 84, 69, 170, 120, 67, 188, 139, 242, 6, 177, 32, 0, 16, 0, 0} + tests := []struct { + header string + wantTraceID trace.TraceID + wantTraceOptions trace.TraceOptions + }{ + { + header: "105445aa7843bc8bf206b12000100000000000000000000000", + wantTraceID: traceID, + wantTraceOptions: trace.TraceOptions(0), + }, + { + header: "105445aa7843bc8bf206b12000100000000000000000000001", + wantTraceID: traceID, + wantTraceOptions: trace.TraceOptions(1), + }, + } + + for _, tt := range tests { + t.Run(tt.header, func(t *testing.T) { + handler := &Handler{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + span := trace.FromContext(r.Context()) + sc := span.SpanContext() + if got, want := sc.TraceID, tt.wantTraceID; got != want { + t.Errorf("TraceID = %q; want %q", got, want) + } + if got, want := sc.TraceOptions, tt.wantTraceOptions; got != want { + t.Errorf("TraceOptions = %v; want %v", got, want) + } + }), + StartOptions: trace.StartOptions{Sampler: trace.ProbabilitySampler(0.0)}, + Propagation: &testPropagator{}, + } + req, _ := http.NewRequest("GET", "http://foo.com", nil) + req.Header.Add("trace", tt.header) + handler.ServeHTTP(nil, req) + }) + } +} + +var _ http.RoundTripper = (*traceTransport)(nil) + +type collector []*trace.SpanData + +func (c *collector) ExportSpan(s *trace.SpanData) { + *c = append(*c, s) +} + +func TestEndToEnd(t *testing.T) { + tc := []struct { + name string + handler *Handler + transport *Transport + wantSameTraceID bool + wantLinks bool // expect a link between client and server span + }{ + { + name: "internal default propagation", + handler: &Handler{}, + transport: &Transport{}, + wantSameTraceID: true, + }, + { + name: "external default propagation", + handler: &Handler{IsPublicEndpoint: true}, + transport: &Transport{}, + wantSameTraceID: false, + wantLinks: true, + }, + { + name: "internal TraceContext propagation", + handler: &Handler{Propagation: &tracecontext.HTTPFormat{}}, + transport: &Transport{Propagation: &tracecontext.HTTPFormat{}}, + wantSameTraceID: true, + }, + { + name: "misconfigured propagation", + handler: &Handler{IsPublicEndpoint: true, Propagation: &tracecontext.HTTPFormat{}}, + transport: &Transport{Propagation: &b3.HTTPFormat{}}, + wantSameTraceID: false, + wantLinks: false, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + var spans collector + trace.RegisterExporter(&spans) + defer trace.UnregisterExporter(&spans) + + // Start the server. + serverDone := make(chan struct{}) + serverReturn := make(chan time.Time) + tt.handler.StartOptions.Sampler = trace.AlwaysSample() + url := serveHTTP(tt.handler, serverDone, serverReturn) + + ctx := context.Background() + // Make the request. + req, err := http.NewRequest( + http.MethodPost, + fmt.Sprintf("%s/example/url/path?qparam=val", url), + strings.NewReader("expected-request-body")) + if err != nil { + t.Fatal(err) + } + req = req.WithContext(ctx) + tt.transport.StartOptions.Sampler = trace.AlwaysSample() + c := &http.Client{ + Transport: tt.transport, + } + resp, err := c.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != http.StatusOK { + t.Fatalf("resp.StatusCode = %d", resp.StatusCode) + } + + // Tell the server to return from request handling. + serverReturn <- time.Now().Add(time.Millisecond) + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if got, want := string(respBody), "expected-response"; got != want { + t.Fatalf("respBody = %q; want %q", got, want) + } + + resp.Body.Close() + + <-serverDone + trace.UnregisterExporter(&spans) + + if got, want := len(spans), 2; got != want { + t.Fatalf("len(spans) = %d; want %d", got, want) + } + + var client, server *trace.SpanData + for _, sp := range spans { + switch sp.SpanKind { + case trace.SpanKindClient: + client = sp + if got, want := client.Name, "/example/url/path"; got != want { + t.Errorf("Span name: %q; want %q", got, want) + } + case trace.SpanKindServer: + server = sp + if got, want := server.Name, "/example/url/path"; got != want { + t.Errorf("Span name: %q; want %q", got, want) + } + default: + t.Fatalf("server or client span missing; kind = %v", sp.SpanKind) + } + } + + if tt.wantSameTraceID { + if server.TraceID != client.TraceID { + t.Errorf("TraceID does not match: server.TraceID=%q client.TraceID=%q", server.TraceID, client.TraceID) + } + if !server.HasRemoteParent { + t.Errorf("server span should have remote parent") + } + if server.ParentSpanID != client.SpanID { + t.Errorf("server span should have client span as parent") + } + } + if !tt.wantSameTraceID { + if server.TraceID == client.TraceID { + t.Errorf("TraceID should not be trusted") + } + } + if tt.wantLinks { + if got, want := len(server.Links), 1; got != want { + t.Errorf("len(server.Links) = %d; want %d", got, want) + } else { + link := server.Links[0] + if got, want := link.Type, trace.LinkTypeChild; got != want { + t.Errorf("link.Type = %v; want %v", got, want) + } + } + } + if server.StartTime.Before(client.StartTime) { + t.Errorf("server span starts before client span") + } + if server.EndTime.After(client.EndTime) { + t.Errorf("client span ends before server span") + } + }) + } +} + +func serveHTTP(handler *Handler, done chan struct{}, wait chan time.Time) string { + handler.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.(http.Flusher).Flush() + + // Simulate a slow-responding server. + sleepUntil := <-wait + for time.Now().Before(sleepUntil) { + time.Sleep(sleepUntil.Sub(time.Now())) + } + + io.WriteString(w, "expected-response") + close(done) + }) + server := httptest.NewServer(handler) + go func() { + <-done + server.Close() + }() + return server.URL +} + +func TestSpanNameFromURL(t *testing.T) { + tests := []struct { + u string + want string + }{ + { + u: "http://localhost:80/hello?q=a", + want: "/hello", + }, + { + u: "/a/b?q=c", + want: "/a/b", + }, + } + for _, tt := range tests { + t.Run(tt.u, func(t *testing.T) { + req, err := http.NewRequest("GET", tt.u, nil) + if err != nil { + t.Errorf("url issue = %v", err) + } + if got := spanNameFromURL(req); got != tt.want { + t.Errorf("spanNameFromURL() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFormatSpanName(t *testing.T) { + formatSpanName := func(r *http.Request) string { + return r.Method + " " + r.URL.Path + } + + handler := &Handler{ + Handler: http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + resp.Write([]byte("Hello, world!")) + }), + FormatSpanName: formatSpanName, + } + + server := httptest.NewServer(handler) + defer server.Close() + + client := &http.Client{ + Transport: &Transport{ + FormatSpanName: formatSpanName, + StartOptions: trace.StartOptions{ + Sampler: trace.AlwaysSample(), + }, + }, + } + + tests := []struct { + u string + want string + }{ + { + u: "/hello?q=a", + want: "GET /hello", + }, + { + u: "/a/b?q=c", + want: "GET /a/b", + }, + } + + for _, tt := range tests { + t.Run(tt.u, func(t *testing.T) { + var te testExporter + trace.RegisterExporter(&te) + res, err := client.Get(server.URL + tt.u) + if err != nil { + t.Fatalf("error creating request: %v", err) + } + res.Body.Close() + trace.UnregisterExporter(&te) + if want, got := 2, len(te.spans); want != got { + t.Fatalf("got exported spans %#v, wanted two spans", te.spans) + } + if got := te.spans[0].Name; got != tt.want { + t.Errorf("spanNameFromURL() = %v, want %v", got, tt.want) + } + if got := te.spans[1].Name; got != tt.want { + t.Errorf("spanNameFromURL() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRequestAttributes(t *testing.T) { + tests := []struct { + name string + makeReq func() *http.Request + wantAttrs []trace.Attribute + }{ + { + name: "GET example.com/hello", + makeReq: func() *http.Request { + req, _ := http.NewRequest("GET", "http://example.com:779/hello", nil) + req.Header.Add("User-Agent", "ua") + return req + }, + wantAttrs: []trace.Attribute{ + trace.StringAttribute("http.path", "/hello"), + trace.StringAttribute("http.host", "example.com:779"), + trace.StringAttribute("http.method", "GET"), + trace.StringAttribute("http.user_agent", "ua"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := tt.makeReq() + attrs := requestAttrs(req) + + if got, want := attrs, tt.wantAttrs; !reflect.DeepEqual(got, want) { + t.Errorf("Request attributes = %#v; want %#v", got, want) + } + }) + } +} + +func TestResponseAttributes(t *testing.T) { + tests := []struct { + name string + resp *http.Response + wantAttrs []trace.Attribute + }{ + { + name: "non-zero HTTP 200 response", + resp: &http.Response{StatusCode: 200}, + wantAttrs: []trace.Attribute{ + trace.Int64Attribute("http.status_code", 200), + }, + }, + { + name: "zero HTTP 500 response", + resp: &http.Response{StatusCode: 500}, + wantAttrs: []trace.Attribute{ + trace.Int64Attribute("http.status_code", 500), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + attrs := responseAttrs(tt.resp) + if got, want := attrs, tt.wantAttrs; !reflect.DeepEqual(got, want) { + t.Errorf("Response attributes = %#v; want %#v", got, want) + } + }) + } +} + +func TestStatusUnitTest(t *testing.T) { + tests := []struct { + in int + want trace.Status + }{ + {200, trace.Status{Code: trace.StatusCodeOK, Message: `OK`}}, + {204, trace.Status{Code: trace.StatusCodeOK, Message: `OK`}}, + {100, trace.Status{Code: trace.StatusCodeUnknown, Message: `UNKNOWN`}}, + {500, trace.Status{Code: trace.StatusCodeUnknown, Message: `UNKNOWN`}}, + {404, trace.Status{Code: trace.StatusCodeNotFound, Message: `NOT_FOUND`}}, + {600, trace.Status{Code: trace.StatusCodeUnknown, Message: `UNKNOWN`}}, + {401, trace.Status{Code: trace.StatusCodeUnauthenticated, Message: `UNAUTHENTICATED`}}, + {403, trace.Status{Code: trace.StatusCodePermissionDenied, Message: `PERMISSION_DENIED`}}, + {301, trace.Status{Code: trace.StatusCodeOK, Message: `OK`}}, + {501, trace.Status{Code: trace.StatusCodeUnimplemented, Message: `UNIMPLEMENTED`}}, + } + + for _, tt := range tests { + got, want := TraceStatus(tt.in, ""), tt.want + if got != want { + t.Errorf("status(%d) got = (%#v) want = (%#v)", tt.in, got, want) + } + } +} diff --git a/vendor/go.opencensus.io/stats/benchmark_test.go b/vendor/go.opencensus.io/stats/benchmark_test.go new file mode 100644 index 0000000000..3e0264fa57 --- /dev/null +++ b/vendor/go.opencensus.io/stats/benchmark_test.go @@ -0,0 +1,96 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stats_test + +import ( + "context" + "testing" + + "go.opencensus.io/stats" + _ "go.opencensus.io/stats/view" // enable collection + "go.opencensus.io/tag" +) + +var m = makeMeasure() + +func BenchmarkRecord0(b *testing.B) { + ctx := context.Background() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + stats.Record(ctx) + } +} + +func BenchmarkRecord1(b *testing.B) { + ctx := context.Background() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + stats.Record(ctx, m.M(1)) + } +} + +func BenchmarkRecord8(b *testing.B) { + ctx := context.Background() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + stats.Record(ctx, m.M(1), m.M(1), m.M(1), m.M(1), m.M(1), m.M(1), m.M(1), m.M(1)) + } +} + +func BenchmarkRecord8_Parallel(b *testing.B) { + ctx := context.Background() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + stats.Record(ctx, m.M(1), m.M(1), m.M(1), m.M(1), m.M(1), m.M(1), m.M(1), m.M(1)) + } + }) +} + +func BenchmarkRecord8_8Tags(b *testing.B) { + ctx := context.Background() + key1, _ := tag.NewKey("key1") + key2, _ := tag.NewKey("key2") + key3, _ := tag.NewKey("key3") + key4, _ := tag.NewKey("key4") + key5, _ := tag.NewKey("key5") + key6, _ := tag.NewKey("key6") + key7, _ := tag.NewKey("key7") + key8, _ := tag.NewKey("key8") + + tag.New(ctx, + tag.Insert(key1, "value"), + tag.Insert(key2, "value"), + tag.Insert(key3, "value"), + tag.Insert(key4, "value"), + tag.Insert(key5, "value"), + tag.Insert(key6, "value"), + tag.Insert(key7, "value"), + tag.Insert(key8, "value"), + ) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + stats.Record(ctx, m.M(1), m.M(1), m.M(1), m.M(1), m.M(1), m.M(1), m.M(1), m.M(1)) + } +} + +func makeMeasure() *stats.Int64Measure { + return stats.Int64("m", "test measure", "") +} diff --git a/vendor/go.opencensus.io/stats/doc.go b/vendor/go.opencensus.io/stats/doc.go new file mode 100644 index 0000000000..00d473ee02 --- /dev/null +++ b/vendor/go.opencensus.io/stats/doc.go @@ -0,0 +1,69 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/* +Package stats contains support for OpenCensus stats recording. + +OpenCensus allows users to create typed measures, record measurements, +aggregate the collected data, and export the aggregated data. + +Measures + +A measure represents a type of data point to be tracked and recorded. +For example, latency, request Mb/s, and response Mb/s are measures +to collect from a server. + +Measure constructors such as Int64 and Float64 automatically +register the measure by the given name. Each registered measure needs +to be unique by name. Measures also have a description and a unit. + +Libraries can define and export measures. Application authors can then +create views and collect and break down measures by the tags they are +interested in. + +Recording measurements + +Measurement is a data point to be collected for a measure. For example, +for a latency (ms) measure, 100 is a measurement that represents a 100ms +latency event. Measurements are created from measures with +the current context. Tags from the current context are recorded with the +measurements if they are any. + +Recorded measurements are dropped immediately if no views are registered for them. +There is usually no need to conditionally enable and disable +recording to reduce cost. Recording of measurements is cheap. + +Libraries can always record measurements, and applications can later decide +on which measurements they want to collect by registering views. This allows +libraries to turn on the instrumentation by default. + +Exemplars + +For a given recorded measurement, the associated exemplar is a diagnostic map +that gives more information about the measurement. + +When aggregated using a Distribution aggregation, an exemplar is kept for each +bucket in the Distribution. This allows you to easily find an example of a +measurement that fell into each bucket. + +For example, if you also use the OpenCensus trace package and you +record a measurement with a context that contains a sampled trace span, +then the trace span will be added to the exemplar associated with the measurement. + +When exported to a supporting back end, you should be able to easily navigate +to example traces that fell into each bucket in the Distribution. + +*/ +package stats // import "go.opencensus.io/stats" diff --git a/vendor/go.opencensus.io/stats/example_test.go b/vendor/go.opencensus.io/stats/example_test.go new file mode 100644 index 0000000000..5520eac8e6 --- /dev/null +++ b/vendor/go.opencensus.io/stats/example_test.go @@ -0,0 +1,33 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stats_test + +import ( + "context" + + "go.opencensus.io/stats" +) + +func ExampleRecord() { + ctx := context.Background() + + // Measures are usually declared as package-private global variables. + openConns := stats.Int64("example.com/measure/openconns", "open connections", stats.UnitDimensionless) + + // Instrumented packages call stats.Record() to record measuremens. + stats.Record(ctx, openConns.M(124)) // Record 124 open connections. + + // Without any views or exporters registered, this statement has no observable effects. +} diff --git a/vendor/go.opencensus.io/stats/internal/record.go b/vendor/go.opencensus.io/stats/internal/record.go new file mode 100644 index 0000000000..ed54552054 --- /dev/null +++ b/vendor/go.opencensus.io/stats/internal/record.go @@ -0,0 +1,25 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "go.opencensus.io/tag" +) + +// DefaultRecorder will be called for each Record call. +var DefaultRecorder func(tags *tag.Map, measurement interface{}, attachments map[string]string) + +// SubscriptionReporter reports when a view subscribed with a measure. +var SubscriptionReporter func(measure string) diff --git a/vendor/go.opencensus.io/stats/internal/validation.go b/vendor/go.opencensus.io/stats/internal/validation.go new file mode 100644 index 0000000000..b946667f96 --- /dev/null +++ b/vendor/go.opencensus.io/stats/internal/validation.go @@ -0,0 +1,28 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal // import "go.opencensus.io/stats/internal" + +const ( + MaxNameLength = 255 +) + +func IsPrintable(str string) bool { + for _, r := range str { + if !(r >= ' ' && r <= '~') { + return false + } + } + return true +} diff --git a/vendor/go.opencensus.io/stats/measure.go b/vendor/go.opencensus.io/stats/measure.go new file mode 100644 index 0000000000..64d02b1961 --- /dev/null +++ b/vendor/go.opencensus.io/stats/measure.go @@ -0,0 +1,123 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package stats + +import ( + "sync" + "sync/atomic" +) + +// Measure represents a single numeric value to be tracked and recorded. +// For example, latency, request bytes, and response bytes could be measures +// to collect from a server. +// +// Measures by themselves have no outside effects. In order to be exported, +// the measure needs to be used in a View. If no Views are defined over a +// measure, there is very little cost in recording it. +type Measure interface { + // Name returns the name of this measure. + // + // Measure names are globally unique (among all libraries linked into your program). + // We recommend prefixing the measure name with a domain name relevant to your + // project or application. + // + // Measure names are never sent over the wire or exported to backends. + // They are only used to create Views. + Name() string + + // Description returns the human-readable description of this measure. + Description() string + + // Unit returns the units for the values this measure takes on. + // + // Units are encoded according to the case-sensitive abbreviations from the + // Unified Code for Units of Measure: http://unitsofmeasure.org/ucum.html + Unit() string +} + +// measureDescriptor is the untyped descriptor associated with each measure. +// Int64Measure and Float64Measure wrap measureDescriptor to provide typed +// recording APIs. +// Two Measures with the same name will have the same measureDescriptor. +type measureDescriptor struct { + subs int32 // access atomically + + name string + description string + unit string +} + +func (m *measureDescriptor) subscribe() { + atomic.StoreInt32(&m.subs, 1) +} + +func (m *measureDescriptor) subscribed() bool { + return atomic.LoadInt32(&m.subs) == 1 +} + +// Name returns the name of the measure. +func (m *measureDescriptor) Name() string { + return m.name +} + +// Description returns the description of the measure. +func (m *measureDescriptor) Description() string { + return m.description +} + +// Unit returns the unit of the measure. +func (m *measureDescriptor) Unit() string { + return m.unit +} + +var ( + mu sync.RWMutex + measures = make(map[string]*measureDescriptor) +) + +func registerMeasureHandle(name, desc, unit string) *measureDescriptor { + mu.Lock() + defer mu.Unlock() + + if stored, ok := measures[name]; ok { + return stored + } + m := &measureDescriptor{ + name: name, + description: desc, + unit: unit, + } + measures[name] = m + return m +} + +// Measurement is the numeric value measured when recording stats. Each measure +// provides methods to create measurements of their kind. For example, Int64Measure +// provides M to convert an int64 into a measurement. +type Measurement struct { + v float64 + m *measureDescriptor +} + +// Value returns the value of the Measurement as a float64. +func (m Measurement) Value() float64 { + return m.v +} + +// Measure returns the Measure from which this Measurement was created. +func (m Measurement) Measure() Measure { + return m.m +} diff --git a/vendor/go.opencensus.io/stats/measure_float64.go b/vendor/go.opencensus.io/stats/measure_float64.go new file mode 100644 index 0000000000..acedb21c44 --- /dev/null +++ b/vendor/go.opencensus.io/stats/measure_float64.go @@ -0,0 +1,36 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package stats + +// Float64Measure is a measure for float64 values. +type Float64Measure struct { + *measureDescriptor +} + +// M creates a new float64 measurement. +// Use Record to record measurements. +func (m *Float64Measure) M(v float64) Measurement { + return Measurement{m: m.measureDescriptor, v: v} +} + +// Float64 creates a new measure for float64 values. +// +// See the documentation for interface Measure for more guidance on the +// parameters of this function. +func Float64(name, description, unit string) *Float64Measure { + mi := registerMeasureHandle(name, description, unit) + return &Float64Measure{mi} +} diff --git a/vendor/go.opencensus.io/stats/measure_int64.go b/vendor/go.opencensus.io/stats/measure_int64.go new file mode 100644 index 0000000000..c4243ba749 --- /dev/null +++ b/vendor/go.opencensus.io/stats/measure_int64.go @@ -0,0 +1,36 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package stats + +// Int64Measure is a measure for int64 values. +type Int64Measure struct { + *measureDescriptor +} + +// M creates a new int64 measurement. +// Use Record to record measurements. +func (m *Int64Measure) M(v int64) Measurement { + return Measurement{m: m.measureDescriptor, v: float64(v)} +} + +// Int64 creates a new measure for int64 values. +// +// See the documentation for interface Measure for more guidance on the +// parameters of this function. +func Int64(name, description, unit string) *Int64Measure { + mi := registerMeasureHandle(name, description, unit) + return &Int64Measure{mi} +} diff --git a/vendor/go.opencensus.io/stats/record.go b/vendor/go.opencensus.io/stats/record.go new file mode 100644 index 0000000000..0aced02c30 --- /dev/null +++ b/vendor/go.opencensus.io/stats/record.go @@ -0,0 +1,69 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package stats + +import ( + "context" + + "go.opencensus.io/exemplar" + "go.opencensus.io/stats/internal" + "go.opencensus.io/tag" +) + +func init() { + internal.SubscriptionReporter = func(measure string) { + mu.Lock() + measures[measure].subscribe() + mu.Unlock() + } +} + +// Record records one or multiple measurements with the same context at once. +// If there are any tags in the context, measurements will be tagged with them. +func Record(ctx context.Context, ms ...Measurement) { + recorder := internal.DefaultRecorder + if recorder == nil { + return + } + if len(ms) == 0 { + return + } + record := false + for _, m := range ms { + if m.m.subscribed() { + record = true + break + } + } + if !record { + return + } + recorder(tag.FromContext(ctx), ms, exemplar.AttachmentsFromContext(ctx)) +} + +// RecordWithTags records one or multiple measurements at once. +// +// Measurements will be tagged with the tags in the context mutated by the mutators. +// RecordWithTags is useful if you want to record with tag mutations but don't want +// to propagate the mutations in the context. +func RecordWithTags(ctx context.Context, mutators []tag.Mutator, ms ...Measurement) error { + ctx, err := tag.New(ctx, mutators...) + if err != nil { + return err + } + Record(ctx, ms...) + return nil +} diff --git a/vendor/go.opencensus.io/stats/units.go b/vendor/go.opencensus.io/stats/units.go new file mode 100644 index 0000000000..6931a5f296 --- /dev/null +++ b/vendor/go.opencensus.io/stats/units.go @@ -0,0 +1,25 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package stats + +// Units are encoded according to the case-sensitive abbreviations from the +// Unified Code for Units of Measure: http://unitsofmeasure.org/ucum.html +const ( + UnitNone = "1" // Deprecated: Use UnitDimensionless. + UnitDimensionless = "1" + UnitBytes = "By" + UnitMilliseconds = "ms" +) diff --git a/vendor/go.opencensus.io/stats/view/aggregation.go b/vendor/go.opencensus.io/stats/view/aggregation.go new file mode 100644 index 0000000000..b7f169b4a5 --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/aggregation.go @@ -0,0 +1,120 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package view + +// AggType represents the type of aggregation function used on a View. +type AggType int + +// All available aggregation types. +const ( + AggTypeNone AggType = iota // no aggregation; reserved for future use. + AggTypeCount // the count aggregation, see Count. + AggTypeSum // the sum aggregation, see Sum. + AggTypeDistribution // the distribution aggregation, see Distribution. + AggTypeLastValue // the last value aggregation, see LastValue. +) + +func (t AggType) String() string { + return aggTypeName[t] +} + +var aggTypeName = map[AggType]string{ + AggTypeNone: "None", + AggTypeCount: "Count", + AggTypeSum: "Sum", + AggTypeDistribution: "Distribution", + AggTypeLastValue: "LastValue", +} + +// Aggregation represents a data aggregation method. Use one of the functions: +// Count, Sum, or Distribution to construct an Aggregation. +type Aggregation struct { + Type AggType // Type is the AggType of this Aggregation. + Buckets []float64 // Buckets are the bucket endpoints if this Aggregation represents a distribution, see Distribution. + + newData func() AggregationData +} + +var ( + aggCount = &Aggregation{ + Type: AggTypeCount, + newData: func() AggregationData { + return &CountData{} + }, + } + aggSum = &Aggregation{ + Type: AggTypeSum, + newData: func() AggregationData { + return &SumData{} + }, + } +) + +// Count indicates that data collected and aggregated +// with this method will be turned into a count value. +// For example, total number of accepted requests can be +// aggregated by using Count. +func Count() *Aggregation { + return aggCount +} + +// Sum indicates that data collected and aggregated +// with this method will be summed up. +// For example, accumulated request bytes can be aggregated by using +// Sum. +func Sum() *Aggregation { + return aggSum +} + +// Distribution indicates that the desired aggregation is +// a histogram distribution. +// +// An distribution aggregation may contain a histogram of the values in the +// population. The bucket boundaries for that histogram are described +// by the bounds. This defines len(bounds)+1 buckets. +// +// If len(bounds) >= 2 then the boundaries for bucket index i are: +// +// [-infinity, bounds[i]) for i = 0 +// [bounds[i-1], bounds[i]) for 0 < i < length +// [bounds[i-1], +infinity) for i = length +// +// If len(bounds) is 0 then there is no histogram associated with the +// distribution. There will be a single bucket with boundaries +// (-infinity, +infinity). +// +// If len(bounds) is 1 then there is no finite buckets, and that single +// element is the common boundary of the overflow and underflow buckets. +func Distribution(bounds ...float64) *Aggregation { + return &Aggregation{ + Type: AggTypeDistribution, + Buckets: bounds, + newData: func() AggregationData { + return newDistributionData(bounds) + }, + } +} + +// LastValue only reports the last value recorded using this +// aggregation. All other measurements will be dropped. +func LastValue() *Aggregation { + return &Aggregation{ + Type: AggTypeLastValue, + newData: func() AggregationData { + return &LastValueData{} + }, + } +} diff --git a/vendor/go.opencensus.io/stats/view/aggregation_data.go b/vendor/go.opencensus.io/stats/view/aggregation_data.go new file mode 100644 index 0000000000..960b94601f --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/aggregation_data.go @@ -0,0 +1,235 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package view + +import ( + "math" + + "go.opencensus.io/exemplar" +) + +// AggregationData represents an aggregated value from a collection. +// They are reported on the view data during exporting. +// Mosts users won't directly access aggregration data. +type AggregationData interface { + isAggregationData() bool + addSample(e *exemplar.Exemplar) + clone() AggregationData + equal(other AggregationData) bool +} + +const epsilon = 1e-9 + +// CountData is the aggregated data for the Count aggregation. +// A count aggregation processes data and counts the recordings. +// +// Most users won't directly access count data. +type CountData struct { + Value int64 +} + +func (a *CountData) isAggregationData() bool { return true } + +func (a *CountData) addSample(_ *exemplar.Exemplar) { + a.Value = a.Value + 1 +} + +func (a *CountData) clone() AggregationData { + return &CountData{Value: a.Value} +} + +func (a *CountData) equal(other AggregationData) bool { + a2, ok := other.(*CountData) + if !ok { + return false + } + + return a.Value == a2.Value +} + +// SumData is the aggregated data for the Sum aggregation. +// A sum aggregation processes data and sums up the recordings. +// +// Most users won't directly access sum data. +type SumData struct { + Value float64 +} + +func (a *SumData) isAggregationData() bool { return true } + +func (a *SumData) addSample(e *exemplar.Exemplar) { + a.Value += e.Value +} + +func (a *SumData) clone() AggregationData { + return &SumData{Value: a.Value} +} + +func (a *SumData) equal(other AggregationData) bool { + a2, ok := other.(*SumData) + if !ok { + return false + } + return math.Pow(a.Value-a2.Value, 2) < epsilon +} + +// DistributionData is the aggregated data for the +// Distribution aggregation. +// +// Most users won't directly access distribution data. +// +// For a distribution with N bounds, the associated DistributionData will have +// N+1 buckets. +type DistributionData struct { + Count int64 // number of data points aggregated + Min float64 // minimum value in the distribution + Max float64 // max value in the distribution + Mean float64 // mean of the distribution + SumOfSquaredDev float64 // sum of the squared deviation from the mean + CountPerBucket []int64 // number of occurrences per bucket + // ExemplarsPerBucket is slice the same length as CountPerBucket containing + // an exemplar for the associated bucket, or nil. + ExemplarsPerBucket []*exemplar.Exemplar + bounds []float64 // histogram distribution of the values +} + +func newDistributionData(bounds []float64) *DistributionData { + bucketCount := len(bounds) + 1 + return &DistributionData{ + CountPerBucket: make([]int64, bucketCount), + ExemplarsPerBucket: make([]*exemplar.Exemplar, bucketCount), + bounds: bounds, + Min: math.MaxFloat64, + Max: math.SmallestNonzeroFloat64, + } +} + +// Sum returns the sum of all samples collected. +func (a *DistributionData) Sum() float64 { return a.Mean * float64(a.Count) } + +func (a *DistributionData) variance() float64 { + if a.Count <= 1 { + return 0 + } + return a.SumOfSquaredDev / float64(a.Count-1) +} + +func (a *DistributionData) isAggregationData() bool { return true } + +func (a *DistributionData) addSample(e *exemplar.Exemplar) { + f := e.Value + if f < a.Min { + a.Min = f + } + if f > a.Max { + a.Max = f + } + a.Count++ + a.addToBucket(e) + + if a.Count == 1 { + a.Mean = f + return + } + + oldMean := a.Mean + a.Mean = a.Mean + (f-a.Mean)/float64(a.Count) + a.SumOfSquaredDev = a.SumOfSquaredDev + (f-oldMean)*(f-a.Mean) +} + +func (a *DistributionData) addToBucket(e *exemplar.Exemplar) { + var count *int64 + var ex **exemplar.Exemplar + for i, b := range a.bounds { + if e.Value < b { + count = &a.CountPerBucket[i] + ex = &a.ExemplarsPerBucket[i] + break + } + } + if count == nil { + count = &a.CountPerBucket[len(a.bounds)] + ex = &a.ExemplarsPerBucket[len(a.bounds)] + } + *count++ + *ex = maybeRetainExemplar(*ex, e) +} + +func maybeRetainExemplar(old, cur *exemplar.Exemplar) *exemplar.Exemplar { + if old == nil { + return cur + } + + // Heuristic to pick the "better" exemplar: first keep the one with a + // sampled trace attachment, if neither have a trace attachment, pick the + // one with more attachments. + _, haveTraceID := cur.Attachments[exemplar.KeyTraceID] + if haveTraceID || len(cur.Attachments) >= len(old.Attachments) { + return cur + } + return old +} + +func (a *DistributionData) clone() AggregationData { + c := *a + c.CountPerBucket = append([]int64(nil), a.CountPerBucket...) + c.ExemplarsPerBucket = append([]*exemplar.Exemplar(nil), a.ExemplarsPerBucket...) + return &c +} + +func (a *DistributionData) equal(other AggregationData) bool { + a2, ok := other.(*DistributionData) + if !ok { + return false + } + if a2 == nil { + return false + } + if len(a.CountPerBucket) != len(a2.CountPerBucket) { + return false + } + for i := range a.CountPerBucket { + if a.CountPerBucket[i] != a2.CountPerBucket[i] { + return false + } + } + return a.Count == a2.Count && a.Min == a2.Min && a.Max == a2.Max && math.Pow(a.Mean-a2.Mean, 2) < epsilon && math.Pow(a.variance()-a2.variance(), 2) < epsilon +} + +// LastValueData returns the last value recorded for LastValue aggregation. +type LastValueData struct { + Value float64 +} + +func (l *LastValueData) isAggregationData() bool { + return true +} + +func (l *LastValueData) addSample(e *exemplar.Exemplar) { + l.Value = e.Value +} + +func (l *LastValueData) clone() AggregationData { + return &LastValueData{l.Value} +} + +func (l *LastValueData) equal(other AggregationData) bool { + a2, ok := other.(*LastValueData) + if !ok { + return false + } + return l.Value == a2.Value +} diff --git a/vendor/go.opencensus.io/stats/view/aggregation_data_test.go b/vendor/go.opencensus.io/stats/view/aggregation_data_test.go new file mode 100644 index 0000000000..9b12b85378 --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/aggregation_data_test.go @@ -0,0 +1,145 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package view + +import ( + "reflect" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "go.opencensus.io/exemplar" +) + +func TestDataClone(t *testing.T) { + dist := newDistributionData([]float64{1, 2, 3, 4}) + dist.Count = 7 + dist.Max = 11 + dist.Min = 1 + dist.CountPerBucket = []int64{0, 2, 3, 2} + dist.Mean = 4 + dist.SumOfSquaredDev = 1.2 + + tests := []struct { + name string + src AggregationData + }{ + { + name: "count data", + src: &CountData{Value: 5}, + }, + { + name: "distribution data", + src: dist, + }, + { + name: "sum data", + src: &SumData{Value: 65.7}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.src.clone() + if !reflect.DeepEqual(got, tt.src) { + t.Errorf("AggregationData.clone() = %v, want %v", got, tt.src) + } + // TODO(jbd): Make sure that data is deep copied. + if got == tt.src { + t.Errorf("AggregationData.clone() returned the same pointer") + } + }) + } +} + +func TestDistributionData_addSample(t *testing.T) { + dd := newDistributionData([]float64{0, 1, 2}) + t1, _ := time.Parse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Jan 2 15:04:05 -0700 MST 2006") + e1 := &exemplar.Exemplar{ + Attachments: exemplar.Attachments{ + "tag:X": "Y", + "tag:A": "B", + }, + Timestamp: t1, + Value: 0.5, + } + dd.addSample(e1) + + want := &DistributionData{ + Count: 1, + CountPerBucket: []int64{0, 1, 0, 0}, + ExemplarsPerBucket: []*exemplar.Exemplar{nil, e1, nil, nil}, + Max: 0.5, + Min: 0.5, + Mean: 0.5, + SumOfSquaredDev: 0, + } + if diff := cmpDD(dd, want); diff != "" { + t.Fatalf("Unexpected DistributionData -got +want: %s", diff) + } + + t2 := t1.Add(time.Microsecond) + e2 := &exemplar.Exemplar{ + Attachments: exemplar.Attachments{ + "tag:X": "Y", + }, + Timestamp: t2, + Value: 0.7, + } + dd.addSample(e2) + + // Previous exemplar should be preserved, since it has more annotations. + want = &DistributionData{ + Count: 2, + CountPerBucket: []int64{0, 2, 0, 0}, + ExemplarsPerBucket: []*exemplar.Exemplar{nil, e1, nil, nil}, + Max: 0.7, + Min: 0.5, + Mean: 0.6, + SumOfSquaredDev: 0, + } + if diff := cmpDD(dd, want); diff != "" { + t.Fatalf("Unexpected DistributionData -got +want: %s", diff) + } + + t3 := t2.Add(time.Microsecond) + e3 := &exemplar.Exemplar{ + Attachments: exemplar.Attachments{ + exemplar.KeyTraceID: "abcd", + }, + Timestamp: t3, + Value: 0.2, + } + dd.addSample(e3) + + // Exemplar should be replaced since it has a trace_id. + want = &DistributionData{ + Count: 3, + CountPerBucket: []int64{0, 3, 0, 0}, + ExemplarsPerBucket: []*exemplar.Exemplar{nil, e3, nil, nil}, + Max: 0.7, + Min: 0.2, + Mean: 0.4666666666666667, + SumOfSquaredDev: 0, + } + if diff := cmpDD(dd, want); diff != "" { + t.Fatalf("Unexpected DistributionData -got +want: %s", diff) + } +} + +func cmpDD(got, want *DistributionData) string { + return cmp.Diff(got, want, cmpopts.IgnoreFields(DistributionData{}, "SumOfSquaredDev"), cmpopts.IgnoreUnexported(DistributionData{})) +} diff --git a/vendor/go.opencensus.io/stats/view/benchmark_test.go b/vendor/go.opencensus.io/stats/view/benchmark_test.go new file mode 100644 index 0000000000..0f195d43b2 --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/benchmark_test.go @@ -0,0 +1,92 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package view + +import ( + "context" + "fmt" + "testing" + "time" + + "go.opencensus.io/stats" + "go.opencensus.io/tag" +) + +var ( + m = stats.Float64("m", "", "") + k1, _ = tag.NewKey("k1") + k2, _ = tag.NewKey("k2") + k3, _ = tag.NewKey("k3") + k4, _ = tag.NewKey("k4") + k5, _ = tag.NewKey("k5") + k6, _ = tag.NewKey("k6") + k7, _ = tag.NewKey("k7") + k8, _ = tag.NewKey("k8") + view = &View{ + Measure: m, + Aggregation: Distribution(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), + TagKeys: []tag.Key{k1, k2}, + } +) + +// BenchmarkRecordReqCommand benchmarks calling the internal recording machinery +// directly. +func BenchmarkRecordReqCommand(b *testing.B) { + w := newWorker() + + register := ®isterViewReq{views: []*View{view}, err: make(chan error, 1)} + register.handleCommand(w) + if err := <-register.err; err != nil { + b.Fatal(err) + } + + const tagCount = 10 + ctxs := make([]context.Context, 0, tagCount) + for i := 0; i < tagCount; i++ { + ctx, _ := tag.New(context.Background(), + tag.Upsert(k1, fmt.Sprintf("v%d", i)), + tag.Upsert(k2, fmt.Sprintf("v%d", i)), + tag.Upsert(k3, fmt.Sprintf("v%d", i)), + tag.Upsert(k4, fmt.Sprintf("v%d", i)), + tag.Upsert(k5, fmt.Sprintf("v%d", i)), + tag.Upsert(k6, fmt.Sprintf("v%d", i)), + tag.Upsert(k7, fmt.Sprintf("v%d", i)), + tag.Upsert(k8, fmt.Sprintf("v%d", i)), + ) + ctxs = append(ctxs, ctx) + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + record := &recordReq{ + ms: []stats.Measurement{ + m.M(1), + m.M(1), + m.M(1), + m.M(1), + m.M(1), + m.M(1), + m.M(1), + m.M(1), + }, + tm: tag.FromContext(ctxs[i%len(ctxs)]), + t: time.Now(), + } + record.handleCommand(w) + } +} diff --git a/vendor/go.opencensus.io/stats/view/collector.go b/vendor/go.opencensus.io/stats/view/collector.go new file mode 100644 index 0000000000..32415d4859 --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/collector.go @@ -0,0 +1,87 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package view + +import ( + "sort" + + "go.opencensus.io/exemplar" + + "go.opencensus.io/internal/tagencoding" + "go.opencensus.io/tag" +) + +type collector struct { + // signatures holds the aggregations values for each unique tag signature + // (values for all keys) to its aggregator. + signatures map[string]AggregationData + // Aggregation is the description of the aggregation to perform for this + // view. + a *Aggregation +} + +func (c *collector) addSample(s string, e *exemplar.Exemplar) { + aggregator, ok := c.signatures[s] + if !ok { + aggregator = c.a.newData() + c.signatures[s] = aggregator + } + aggregator.addSample(e) +} + +// collectRows returns a snapshot of the collected Row values. +func (c *collector) collectedRows(keys []tag.Key) []*Row { + rows := make([]*Row, 0, len(c.signatures)) + for sig, aggregator := range c.signatures { + tags := decodeTags([]byte(sig), keys) + row := &Row{Tags: tags, Data: aggregator.clone()} + rows = append(rows, row) + } + return rows +} + +func (c *collector) clearRows() { + c.signatures = make(map[string]AggregationData) +} + +// encodeWithKeys encodes the map by using values +// only associated with the keys provided. +func encodeWithKeys(m *tag.Map, keys []tag.Key) []byte { + vb := &tagencoding.Values{ + Buffer: make([]byte, len(keys)), + } + for _, k := range keys { + v, _ := m.Value(k) + vb.WriteValue([]byte(v)) + } + return vb.Bytes() +} + +// decodeTags decodes tags from the buffer and +// orders them by the keys. +func decodeTags(buf []byte, keys []tag.Key) []tag.Tag { + vb := &tagencoding.Values{Buffer: buf} + var tags []tag.Tag + for _, k := range keys { + v := vb.ReadValue() + if v != nil { + tags = append(tags, tag.Tag{Key: k, Value: string(v)}) + } + } + vb.ReadIndex = 0 + sort.Slice(tags, func(i, j int) bool { return tags[i].Key.Name() < tags[j].Key.Name() }) + return tags +} diff --git a/vendor/go.opencensus.io/stats/view/collector_test.go b/vendor/go.opencensus.io/stats/view/collector_test.go new file mode 100644 index 0000000000..57720c10ad --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/collector_test.go @@ -0,0 +1,118 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package view + +import ( + "context" + "testing" + + "go.opencensus.io/tag" +) + +func TestEncodeDecodeTags(t *testing.T) { + ctx := context.Background() + type testData struct { + m *tag.Map + keys []tag.Key + want map[tag.Key][]byte + } + + k1, err := tag.NewKey("/encodedecodetest/k1") + if err != nil { + t.Fatal(err) + } + k2, err := tag.NewKey("/encodedecodetest/k2") + if err != nil { + t.Fatal(err) + } + k3, err := tag.NewKey("/encodedecodetest/k3") + if err != nil { + t.Fatal(err) + } + + ctx1, _ := tag.New(ctx) + ctx2, _ := tag.New(ctx, tag.Insert(k2, "v2")) + ctx3, _ := tag.New(ctx, tag.Insert(k1, "v1"), tag.Insert(k2, "v2")) + ctx4, _ := tag.New(ctx, tag.Insert(k1, "v1"), tag.Insert(k2, "v2"), tag.Insert(k3, "v3")) + + m1 := tag.FromContext(ctx1) + m2 := tag.FromContext(ctx2) + m3 := tag.FromContext(ctx3) + m4 := tag.FromContext(ctx4) + + tests := []testData{ + { + m1, + []tag.Key{k1}, + nil, + }, + { + m2, + []tag.Key{}, + nil, + }, + { + m2, + []tag.Key{k1}, + nil, + }, + { + m2, + []tag.Key{k2}, + map[tag.Key][]byte{ + k2: []byte("v2"), + }, + }, + { + m3, + []tag.Key{k1}, + map[tag.Key][]byte{ + k1: []byte("v1"), + }, + }, + { + m3, + []tag.Key{k1, k2}, + map[tag.Key][]byte{ + k1: []byte("v1"), + k2: []byte("v2"), + }, + }, + { + m4, + []tag.Key{k3, k1}, + map[tag.Key][]byte{ + k1: []byte("v1"), + k3: []byte("v3"), + }, + }, + } + + for label, tt := range tests { + tags := decodeTags(encodeWithKeys(tt.m, tt.keys), tt.keys) + if got, want := len(tags), len(tt.want); got != want { + t.Fatalf("%d: len(decoded) = %v; not %v", label, got, want) + } + + for _, tag := range tags { + if _, ok := tt.want[tag.Key]; !ok { + t.Errorf("%d: missing key %v", label, tag.Key) + } + if got, want := tag.Value, string(tt.want[tag.Key]); got != want { + t.Errorf("%d: got value %q; want %q", label, got, want) + } + } + } +} diff --git a/vendor/go.opencensus.io/stats/view/doc.go b/vendor/go.opencensus.io/stats/view/doc.go new file mode 100644 index 0000000000..dced225c3d --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/doc.go @@ -0,0 +1,47 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Package view contains support for collecting and exposing aggregates over stats. +// +// In order to collect measurements, views need to be defined and registered. +// A view allows recorded measurements to be filtered and aggregated. +// +// All recorded measurements can be grouped by a list of tags. +// +// OpenCensus provides several aggregation methods: Count, Distribution and Sum. +// +// Count only counts the number of measurement points recorded. +// Distribution provides statistical summary of the aggregated data by counting +// how many recorded measurements fall into each bucket. +// Sum adds up the measurement values. +// LastValue just keeps track of the most recently recorded measurement value. +// All aggregations are cumulative. +// +// Views can be registerd and unregistered at any time during program execution. +// +// Libraries can define views but it is recommended that in most cases registering +// views be left up to applications. +// +// Exporting +// +// Collected and aggregated data can be exported to a metric collection +// backend by registering its exporter. +// +// Multiple exporters can be registered to upload the data to various +// different back ends. +package view // import "go.opencensus.io/stats/view" + +// TODO(acetechnologist): Add a link to the language independent OpenCensus +// spec when it is available. diff --git a/vendor/go.opencensus.io/stats/view/example_test.go b/vendor/go.opencensus.io/stats/view/example_test.go new file mode 100644 index 0000000000..78556a3139 --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/example_test.go @@ -0,0 +1,39 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package view_test + +import ( + "log" + + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" +) + +func Example() { + // Measures are usually declared and used by instrumented packages. + m := stats.Int64("example.com/measure/openconns", "open connections", stats.UnitDimensionless) + + // Views are usually registered in your application main function. + if err := view.Register(&view.View{ + Name: "example.com/views/openconns", + Description: "open connections", + Measure: m, + Aggregation: view.Distribution(0, 1000, 2000), + }); err != nil { + log.Fatal(err) + } + + // Use view.RegisterExporter to export collected data. +} diff --git a/vendor/go.opencensus.io/stats/view/export.go b/vendor/go.opencensus.io/stats/view/export.go new file mode 100644 index 0000000000..7cb59718f5 --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/export.go @@ -0,0 +1,58 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package view + +import "sync" + +var ( + exportersMu sync.RWMutex // guards exporters + exporters = make(map[Exporter]struct{}) +) + +// Exporter exports the collected records as view data. +// +// The ExportView method should return quickly; if an +// Exporter takes a significant amount of time to +// process a Data, that work should be done on another goroutine. +// +// It is safe to assume that ExportView will not be called concurrently from +// multiple goroutines. +// +// The Data should not be modified. +type Exporter interface { + ExportView(viewData *Data) +} + +// RegisterExporter registers an exporter. +// Collected data will be reported via all the +// registered exporters. Once you no longer +// want data to be exported, invoke UnregisterExporter +// with the previously registered exporter. +// +// Binaries can register exporters, libraries shouldn't register exporters. +func RegisterExporter(e Exporter) { + exportersMu.Lock() + defer exportersMu.Unlock() + + exporters[e] = struct{}{} +} + +// UnregisterExporter unregisters an exporter. +func UnregisterExporter(e Exporter) { + exportersMu.Lock() + defer exportersMu.Unlock() + + delete(exporters, e) +} diff --git a/vendor/go.opencensus.io/stats/view/view.go b/vendor/go.opencensus.io/stats/view/view.go new file mode 100644 index 0000000000..c2a08af678 --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/view.go @@ -0,0 +1,185 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package view + +import ( + "bytes" + "fmt" + "reflect" + "sort" + "sync/atomic" + "time" + + "go.opencensus.io/exemplar" + + "go.opencensus.io/stats" + "go.opencensus.io/stats/internal" + "go.opencensus.io/tag" +) + +// View allows users to aggregate the recorded stats.Measurements. +// Views need to be passed to the Register function to be before data will be +// collected and sent to Exporters. +type View struct { + Name string // Name of View. Must be unique. If unset, will default to the name of the Measure. + Description string // Description is a human-readable description for this view. + + // TagKeys are the tag keys describing the grouping of this view. + // A single Row will be produced for each combination of associated tag values. + TagKeys []tag.Key + + // Measure is a stats.Measure to aggregate in this view. + Measure stats.Measure + + // Aggregation is the aggregation function tp apply to the set of Measurements. + Aggregation *Aggregation +} + +// WithName returns a copy of the View with a new name. This is useful for +// renaming views to cope with limitations placed on metric names by various +// backends. +func (v *View) WithName(name string) *View { + vNew := *v + vNew.Name = name + return &vNew +} + +// same compares two views and returns true if they represent the same aggregation. +func (v *View) same(other *View) bool { + if v == other { + return true + } + if v == nil { + return false + } + return reflect.DeepEqual(v.Aggregation, other.Aggregation) && + v.Measure.Name() == other.Measure.Name() +} + +// canonicalize canonicalizes v by setting explicit +// defaults for Name and Description and sorting the TagKeys +func (v *View) canonicalize() error { + if v.Measure == nil { + return fmt.Errorf("cannot register view %q: measure not set", v.Name) + } + if v.Aggregation == nil { + return fmt.Errorf("cannot register view %q: aggregation not set", v.Name) + } + if v.Name == "" { + v.Name = v.Measure.Name() + } + if v.Description == "" { + v.Description = v.Measure.Description() + } + if err := checkViewName(v.Name); err != nil { + return err + } + sort.Slice(v.TagKeys, func(i, j int) bool { + return v.TagKeys[i].Name() < v.TagKeys[j].Name() + }) + return nil +} + +// viewInternal is the internal representation of a View. +type viewInternal struct { + view *View // view is the canonicalized View definition associated with this view. + subscribed uint32 // 1 if someone is subscribed and data need to be exported, use atomic to access + collector *collector +} + +func newViewInternal(v *View) (*viewInternal, error) { + return &viewInternal{ + view: v, + collector: &collector{make(map[string]AggregationData), v.Aggregation}, + }, nil +} + +func (v *viewInternal) subscribe() { + atomic.StoreUint32(&v.subscribed, 1) +} + +func (v *viewInternal) unsubscribe() { + atomic.StoreUint32(&v.subscribed, 0) +} + +// isSubscribed returns true if the view is exporting +// data by subscription. +func (v *viewInternal) isSubscribed() bool { + return atomic.LoadUint32(&v.subscribed) == 1 +} + +func (v *viewInternal) clearRows() { + v.collector.clearRows() +} + +func (v *viewInternal) collectedRows() []*Row { + return v.collector.collectedRows(v.view.TagKeys) +} + +func (v *viewInternal) addSample(m *tag.Map, e *exemplar.Exemplar) { + if !v.isSubscribed() { + return + } + sig := string(encodeWithKeys(m, v.view.TagKeys)) + v.collector.addSample(sig, e) +} + +// A Data is a set of rows about usage of the single measure associated +// with the given view. Each row is specific to a unique set of tags. +type Data struct { + View *View + Start, End time.Time + Rows []*Row +} + +// Row is the collected value for a specific set of key value pairs a.k.a tags. +type Row struct { + Tags []tag.Tag + Data AggregationData +} + +func (r *Row) String() string { + var buffer bytes.Buffer + buffer.WriteString("{ ") + buffer.WriteString("{ ") + for _, t := range r.Tags { + buffer.WriteString(fmt.Sprintf("{%v %v}", t.Key.Name(), t.Value)) + } + buffer.WriteString(" }") + buffer.WriteString(fmt.Sprintf("%v", r.Data)) + buffer.WriteString(" }") + return buffer.String() +} + +// Equal returns true if both rows are equal. Tags are expected to be ordered +// by the key name. Even both rows have the same tags but the tags appear in +// different orders it will return false. +func (r *Row) Equal(other *Row) bool { + if r == other { + return true + } + return reflect.DeepEqual(r.Tags, other.Tags) && r.Data.equal(other.Data) +} + +func checkViewName(name string) error { + if len(name) > internal.MaxNameLength { + return fmt.Errorf("view name cannot be larger than %v", internal.MaxNameLength) + } + if !internal.IsPrintable(name) { + return fmt.Errorf("view name needs to be an ASCII string") + } + return nil +} diff --git a/vendor/go.opencensus.io/stats/view/view_measure_test.go b/vendor/go.opencensus.io/stats/view/view_measure_test.go new file mode 100644 index 0000000000..6ee8413d44 --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/view_measure_test.go @@ -0,0 +1,50 @@ +package view + +import ( + "context" + "testing" + + "go.opencensus.io/stats" +) + +func TestMeasureFloat64AndInt64(t *testing.T) { + // Recording through both a Float64Measure and Int64Measure with the + // same name should work. + + im := stats.Int64("TestMeasureFloat64AndInt64", "", stats.UnitDimensionless) + fm := stats.Float64("TestMeasureFloat64AndInt64", "", stats.UnitDimensionless) + + if im == nil || fm == nil { + t.Fatal("Error creating Measures") + } + + v1 := &View{ + Name: "TestMeasureFloat64AndInt64/v1", + Measure: im, + Aggregation: Sum(), + } + v2 := &View{ + Name: "TestMeasureFloat64AndInt64/v2", + Measure: fm, + Aggregation: Sum(), + } + Register(v1, v2) + + stats.Record(context.Background(), im.M(5)) + stats.Record(context.Background(), fm.M(2.2)) + + d1, _ := RetrieveData(v1.Name) + d2, _ := RetrieveData(v2.Name) + + sum1 := d1[0].Data.(*SumData) + sum2 := d2[0].Data.(*SumData) + + // We expect both views to return 7.2, as though we recorded on a single measure. + + if got, want := sum1.Value, 7.2; got != want { + t.Errorf("sum1 = %v; want %v", got, want) + } + if got, want := sum2.Value, 7.2; got != want { + t.Errorf("sum2 = %v; want %v", got, want) + } +} diff --git a/vendor/go.opencensus.io/stats/view/view_test.go b/vendor/go.opencensus.io/stats/view/view_test.go new file mode 100644 index 0000000000..445e56a1eb --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/view_test.go @@ -0,0 +1,446 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package view + +import ( + "context" + "testing" + + "go.opencensus.io/exemplar" + + "go.opencensus.io/stats" + "go.opencensus.io/tag" +) + +func Test_View_MeasureFloat64_AggregationDistribution(t *testing.T) { + k1, _ := tag.NewKey("k1") + k2, _ := tag.NewKey("k2") + k3, _ := tag.NewKey("k3") + agg1 := Distribution(2) + m := stats.Int64("Test_View_MeasureFloat64_AggregationDistribution/m1", "", stats.UnitDimensionless) + view1 := &View{ + TagKeys: []tag.Key{k1, k2}, + Measure: m, + Aggregation: agg1, + } + view, err := newViewInternal(view1) + if err != nil { + t.Fatal(err) + } + + type tagString struct { + k tag.Key + v string + } + type record struct { + f float64 + tags []tagString + } + + type testCase struct { + label string + records []record + wantRows []*Row + } + + tcs := []testCase{ + { + "1", + []record{ + {1, []tagString{{k1, "v1"}}}, + {5, []tagString{{k1, "v1"}}}, + }, + []*Row{ + { + []tag.Tag{{Key: k1, Value: "v1"}}, + &DistributionData{ + Count: 2, Min: 1, Max: 5, Mean: 3, SumOfSquaredDev: 8, CountPerBucket: []int64{1, 1}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil}, + }, + }, + }, + }, + { + "2", + []record{ + {1, []tagString{{k1, "v1"}}}, + {5, []tagString{{k2, "v2"}}}, + }, + []*Row{ + { + []tag.Tag{{Key: k1, Value: "v1"}}, + &DistributionData{ + Count: 1, Min: 1, Max: 1, Mean: 1, CountPerBucket: []int64{1, 0}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil}, + }, + }, + { + []tag.Tag{{Key: k2, Value: "v2"}}, + &DistributionData{ + Count: 1, Min: 5, Max: 5, Mean: 5, CountPerBucket: []int64{0, 1}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil}, + }, + }, + }, + }, + { + "3", + []record{ + {1, []tagString{{k1, "v1"}}}, + {5, []tagString{{k1, "v1"}, {k3, "v3"}}}, + {1, []tagString{{k1, "v1 other"}}}, + {5, []tagString{{k2, "v2"}}}, + {5, []tagString{{k1, "v1"}, {k2, "v2"}}}, + }, + []*Row{ + { + []tag.Tag{{Key: k1, Value: "v1"}}, + &DistributionData{ + Count: 2, Min: 1, Max: 5, Mean: 3, SumOfSquaredDev: 8, CountPerBucket: []int64{1, 1}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil}, + }, + }, + { + []tag.Tag{{Key: k1, Value: "v1 other"}}, + &DistributionData{ + Count: 1, Min: 1, Max: 1, Mean: 1, CountPerBucket: []int64{1, 0}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil}, + }, + }, + { + []tag.Tag{{Key: k2, Value: "v2"}}, + &DistributionData{ + Count: 1, Min: 5, Max: 5, Mean: 5, CountPerBucket: []int64{0, 1}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil}, + }, + }, + { + []tag.Tag{{Key: k1, Value: "v1"}, {Key: k2, Value: "v2"}}, + &DistributionData{ + Count: 1, Min: 5, Max: 5, Mean: 5, CountPerBucket: []int64{0, 1}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil}, + }, + }, + }, + }, + { + "4", + []record{ + {1, []tagString{{k1, "v1 is a very long value key"}}}, + {5, []tagString{{k1, "v1 is a very long value key"}, {k3, "v3"}}}, + {1, []tagString{{k1, "v1 is another very long value key"}}}, + {1, []tagString{{k1, "v1 is a very long value key"}, {k2, "v2 is a very long value key"}}}, + {5, []tagString{{k1, "v1 is a very long value key"}, {k2, "v2 is a very long value key"}}}, + {3, []tagString{{k1, "v1 is a very long value key"}, {k2, "v2 is a very long value key"}}}, + {3, []tagString{{k1, "v1 is a very long value key"}, {k2, "v2 is a very long value key"}}}, + }, + []*Row{ + { + []tag.Tag{{Key: k1, Value: "v1 is a very long value key"}}, + &DistributionData{ + Count: 2, Min: 1, Max: 5, Mean: 3, SumOfSquaredDev: 8, CountPerBucket: []int64{1, 1}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil}, + }, + }, + { + []tag.Tag{{Key: k1, Value: "v1 is another very long value key"}}, + &DistributionData{ + Count: 1, Min: 1, Max: 1, Mean: 1, CountPerBucket: []int64{1, 0}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil}, + }, + }, + { + []tag.Tag{{Key: k1, Value: "v1 is a very long value key"}, {Key: k2, Value: "v2 is a very long value key"}}, + &DistributionData{ + Count: 4, Min: 1, Max: 5, Mean: 3, SumOfSquaredDev: 2.66666666666667 * 3, CountPerBucket: []int64{1, 3}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil}, + }, + }, + }, + }, + } + + for _, tc := range tcs { + view.clearRows() + view.subscribe() + for _, r := range tc.records { + mods := []tag.Mutator{} + for _, t := range r.tags { + mods = append(mods, tag.Insert(t.k, t.v)) + } + ctx, err := tag.New(context.Background(), mods...) + if err != nil { + t.Errorf("%v: New = %v", tc.label, err) + } + e := &exemplar.Exemplar{ + Value: r.f, + Attachments: exemplar.AttachmentsFromContext(ctx), + } + view.addSample(tag.FromContext(ctx), e) + } + + gotRows := view.collectedRows() + for i, got := range gotRows { + if !containsRow(tc.wantRows, got) { + t.Errorf("%v-%d: got row %v; want none", tc.label, i, got) + break + } + } + + for i, want := range tc.wantRows { + if !containsRow(gotRows, want) { + t.Errorf("%v-%d: got none; want row %v", tc.label, i, want) + break + } + } + } +} + +func Test_View_MeasureFloat64_AggregationSum(t *testing.T) { + k1, _ := tag.NewKey("k1") + k2, _ := tag.NewKey("k2") + k3, _ := tag.NewKey("k3") + m := stats.Int64("Test_View_MeasureFloat64_AggregationSum/m1", "", stats.UnitDimensionless) + view, err := newViewInternal(&View{TagKeys: []tag.Key{k1, k2}, Measure: m, Aggregation: Sum()}) + if err != nil { + t.Fatal(err) + } + + type tagString struct { + k tag.Key + v string + } + type record struct { + f float64 + tags []tagString + } + + tcs := []struct { + label string + records []record + wantRows []*Row + }{ + { + "1", + []record{ + {1, []tagString{{k1, "v1"}}}, + {5, []tagString{{k1, "v1"}}}, + }, + []*Row{ + { + []tag.Tag{{Key: k1, Value: "v1"}}, + &SumData{Value: 6}, + }, + }, + }, + { + "2", + []record{ + {1, []tagString{{k1, "v1"}}}, + {5, []tagString{{k2, "v2"}}}, + }, + []*Row{ + { + []tag.Tag{{Key: k1, Value: "v1"}}, + &SumData{Value: 1}, + }, + { + []tag.Tag{{Key: k2, Value: "v2"}}, + &SumData{Value: 5}, + }, + }, + }, + { + "3", + []record{ + {1, []tagString{{k1, "v1"}}}, + {5, []tagString{{k1, "v1"}, {k3, "v3"}}}, + {1, []tagString{{k1, "v1 other"}}}, + {5, []tagString{{k2, "v2"}}}, + {5, []tagString{{k1, "v1"}, {k2, "v2"}}}, + }, + []*Row{ + { + []tag.Tag{{Key: k1, Value: "v1"}}, + &SumData{Value: 6}, + }, + { + []tag.Tag{{Key: k1, Value: "v1 other"}}, + &SumData{Value: 1}, + }, + { + []tag.Tag{{Key: k2, Value: "v2"}}, + &SumData{Value: 5}, + }, + { + []tag.Tag{{Key: k1, Value: "v1"}, {Key: k2, Value: "v2"}}, + &SumData{Value: 5}, + }, + }, + }, + } + + for _, tt := range tcs { + view.clearRows() + view.subscribe() + for _, r := range tt.records { + mods := []tag.Mutator{} + for _, t := range r.tags { + mods = append(mods, tag.Insert(t.k, t.v)) + } + ctx, err := tag.New(context.Background(), mods...) + if err != nil { + t.Errorf("%v: New = %v", tt.label, err) + } + e := &exemplar.Exemplar{ + Value: r.f, + } + view.addSample(tag.FromContext(ctx), e) + } + + gotRows := view.collectedRows() + for i, got := range gotRows { + if !containsRow(tt.wantRows, got) { + t.Errorf("%v-%d: got row %v; want none", tt.label, i, got) + break + } + } + + for i, want := range tt.wantRows { + if !containsRow(gotRows, want) { + t.Errorf("%v-%d: got none; want row %v", tt.label, i, want) + break + } + } + } +} + +func TestCanonicalize(t *testing.T) { + k1, _ := tag.NewKey("k1") + k2, _ := tag.NewKey("k2") + m := stats.Int64("TestCanonicalize/m1", "desc desc", stats.UnitDimensionless) + v := &View{TagKeys: []tag.Key{k2, k1}, Measure: m, Aggregation: Sum()} + err := v.canonicalize() + if err != nil { + t.Fatal(err) + } + if got, want := v.Name, "TestCanonicalize/m1"; got != want { + t.Errorf("vc.Name = %q; want %q", got, want) + } + if got, want := v.Description, "desc desc"; got != want { + t.Errorf("vc.Description = %q; want %q", got, want) + } + if got, want := len(v.TagKeys), 2; got != want { + t.Errorf("len(vc.TagKeys) = %d; want %d", got, want) + } + if got, want := v.TagKeys[0].Name(), "k1"; got != want { + t.Errorf("vc.TagKeys[0].Name() = %q; want %q", got, want) + } +} + +func TestViewSortedKeys(t *testing.T) { + k1, _ := tag.NewKey("a") + k2, _ := tag.NewKey("b") + k3, _ := tag.NewKey("c") + ks := []tag.Key{k1, k3, k2} + + m := stats.Int64("TestViewSortedKeys/m1", "", stats.UnitDimensionless) + Register(&View{ + Name: "sort_keys", + Description: "desc sort_keys", + TagKeys: ks, + Measure: m, + Aggregation: Sum(), + }) + // Register normalizes the view by sorting the tag keys, retrieve the normalized view + v := Find("sort_keys") + + want := []string{"a", "b", "c"} + vks := v.TagKeys + if len(vks) != len(want) { + t.Errorf("Keys = %+v; want %+v", vks, want) + } + + for i, v := range want { + if got, want := v, vks[i].Name(); got != want { + t.Errorf("View name = %q; want %q", got, want) + } + } +} + +// containsRow returns true if rows contain r. +func containsRow(rows []*Row, r *Row) bool { + for _, x := range rows { + if r.Equal(x) { + return true + } + } + return false +} + +func TestRegisterUnregisterParity(t *testing.T) { + measures := []stats.Measure{ + stats.Int64("ifoo", "iFOO", "iBar"), + stats.Float64("ffoo", "fFOO", "fBar"), + } + aggregations := []*Aggregation{ + Count(), + Sum(), + Distribution(1, 2.0, 4.0, 8.0, 16.0), + } + + for i := 0; i < 10; i++ { + for _, m := range measures { + for _, agg := range aggregations { + v := &View{ + Aggregation: agg, + Name: "Lookup here", + Measure: m, + } + if err := Register(v); err != nil { + t.Errorf("Iteration #%d:\nMeasure: (%#v)\nAggregation (%#v)\nError: %v", i, m, agg, err) + } + Unregister(v) + } + } + } +} + +func TestRegisterAfterMeasurement(t *testing.T) { + // Tests that we can register views after measurements are created and + // they still take effect. + + m := stats.Int64(t.Name(), "", stats.UnitDimensionless) + mm := m.M(1) + ctx := context.Background() + + stats.Record(ctx, mm) + v := &View{ + Measure: m, + Aggregation: Count(), + } + if err := Register(v); err != nil { + t.Fatal(err) + } + + rows, err := RetrieveData(v.Name) + if err != nil { + t.Fatal(err) + } + if len(rows) > 0 { + t.Error("View should not have data") + } + + stats.Record(ctx, mm) + + rows, err = RetrieveData(v.Name) + if err != nil { + t.Fatal(err) + } + if len(rows) == 0 { + t.Error("View should have data") + } +} diff --git a/vendor/go.opencensus.io/stats/view/worker.go b/vendor/go.opencensus.io/stats/view/worker.go new file mode 100644 index 0000000000..63b0ee3cc3 --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/worker.go @@ -0,0 +1,229 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package view + +import ( + "fmt" + "time" + + "go.opencensus.io/stats" + "go.opencensus.io/stats/internal" + "go.opencensus.io/tag" +) + +func init() { + defaultWorker = newWorker() + go defaultWorker.start() + internal.DefaultRecorder = record +} + +type measureRef struct { + measure string + views map[*viewInternal]struct{} +} + +type worker struct { + measures map[string]*measureRef + views map[string]*viewInternal + startTimes map[*viewInternal]time.Time + + timer *time.Ticker + c chan command + quit, done chan bool +} + +var defaultWorker *worker + +var defaultReportingDuration = 10 * time.Second + +// Find returns a registered view associated with this name. +// If no registered view is found, nil is returned. +func Find(name string) (v *View) { + req := &getViewByNameReq{ + name: name, + c: make(chan *getViewByNameResp), + } + defaultWorker.c <- req + resp := <-req.c + return resp.v +} + +// Register begins collecting data for the given views. +// Once a view is registered, it reports data to the registered exporters. +func Register(views ...*View) error { + for _, v := range views { + if err := v.canonicalize(); err != nil { + return err + } + } + req := ®isterViewReq{ + views: views, + err: make(chan error), + } + defaultWorker.c <- req + return <-req.err +} + +// Unregister the given views. Data will not longer be exported for these views +// after Unregister returns. +// It is not necessary to unregister from views you expect to collect for the +// duration of your program execution. +func Unregister(views ...*View) { + names := make([]string, len(views)) + for i := range views { + names[i] = views[i].Name + } + req := &unregisterFromViewReq{ + views: names, + done: make(chan struct{}), + } + defaultWorker.c <- req + <-req.done +} + +// RetrieveData gets a snapshot of the data collected for the the view registered +// with the given name. It is intended for testing only. +func RetrieveData(viewName string) ([]*Row, error) { + req := &retrieveDataReq{ + now: time.Now(), + v: viewName, + c: make(chan *retrieveDataResp), + } + defaultWorker.c <- req + resp := <-req.c + return resp.rows, resp.err +} + +func record(tags *tag.Map, ms interface{}, attachments map[string]string) { + req := &recordReq{ + tm: tags, + ms: ms.([]stats.Measurement), + attachments: attachments, + t: time.Now(), + } + defaultWorker.c <- req +} + +// SetReportingPeriod sets the interval between reporting aggregated views in +// the program. If duration is less than or equal to zero, it enables the +// default behavior. +// +// Note: each exporter makes different promises about what the lowest supported +// duration is. For example, the Stackdriver exporter recommends a value no +// lower than 1 minute. Consult each exporter per your needs. +func SetReportingPeriod(d time.Duration) { + // TODO(acetechnologist): ensure that the duration d is more than a certain + // value. e.g. 1s + req := &setReportingPeriodReq{ + d: d, + c: make(chan bool), + } + defaultWorker.c <- req + <-req.c // don't return until the timer is set to the new duration. +} + +func newWorker() *worker { + return &worker{ + measures: make(map[string]*measureRef), + views: make(map[string]*viewInternal), + startTimes: make(map[*viewInternal]time.Time), + timer: time.NewTicker(defaultReportingDuration), + c: make(chan command, 1024), + quit: make(chan bool), + done: make(chan bool), + } +} + +func (w *worker) start() { + for { + select { + case cmd := <-w.c: + cmd.handleCommand(w) + case <-w.timer.C: + w.reportUsage(time.Now()) + case <-w.quit: + w.timer.Stop() + close(w.c) + w.done <- true + return + } + } +} + +func (w *worker) stop() { + w.quit <- true + <-w.done +} + +func (w *worker) getMeasureRef(name string) *measureRef { + if mr, ok := w.measures[name]; ok { + return mr + } + mr := &measureRef{ + measure: name, + views: make(map[*viewInternal]struct{}), + } + w.measures[name] = mr + return mr +} + +func (w *worker) tryRegisterView(v *View) (*viewInternal, error) { + vi, err := newViewInternal(v) + if err != nil { + return nil, err + } + if x, ok := w.views[vi.view.Name]; ok { + if !x.view.same(vi.view) { + return nil, fmt.Errorf("cannot register view %q; a different view with the same name is already registered", v.Name) + } + + // the view is already registered so there is nothing to do and the + // command is considered successful. + return x, nil + } + w.views[vi.view.Name] = vi + ref := w.getMeasureRef(vi.view.Measure.Name()) + ref.views[vi] = struct{}{} + return vi, nil +} + +func (w *worker) reportView(v *viewInternal, now time.Time) { + if !v.isSubscribed() { + return + } + rows := v.collectedRows() + _, ok := w.startTimes[v] + if !ok { + w.startTimes[v] = now + } + viewData := &Data{ + View: v.view, + Start: w.startTimes[v], + End: time.Now(), + Rows: rows, + } + exportersMu.Lock() + for e := range exporters { + e.ExportView(viewData) + } + exportersMu.Unlock() +} + +func (w *worker) reportUsage(now time.Time) { + for _, v := range w.views { + w.reportView(v, now) + } +} diff --git a/vendor/go.opencensus.io/stats/view/worker_commands.go b/vendor/go.opencensus.io/stats/view/worker_commands.go new file mode 100644 index 0000000000..b38f26f424 --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/worker_commands.go @@ -0,0 +1,183 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package view + +import ( + "errors" + "fmt" + "strings" + "time" + + "go.opencensus.io/exemplar" + + "go.opencensus.io/stats" + "go.opencensus.io/stats/internal" + "go.opencensus.io/tag" +) + +type command interface { + handleCommand(w *worker) +} + +// getViewByNameReq is the command to get a view given its name. +type getViewByNameReq struct { + name string + c chan *getViewByNameResp +} + +type getViewByNameResp struct { + v *View +} + +func (cmd *getViewByNameReq) handleCommand(w *worker) { + v := w.views[cmd.name] + if v == nil { + cmd.c <- &getViewByNameResp{nil} + return + } + cmd.c <- &getViewByNameResp{v.view} +} + +// registerViewReq is the command to register a view. +type registerViewReq struct { + views []*View + err chan error +} + +func (cmd *registerViewReq) handleCommand(w *worker) { + var errstr []string + for _, view := range cmd.views { + vi, err := w.tryRegisterView(view) + if err != nil { + errstr = append(errstr, fmt.Sprintf("%s: %v", view.Name, err)) + continue + } + internal.SubscriptionReporter(view.Measure.Name()) + vi.subscribe() + } + if len(errstr) > 0 { + cmd.err <- errors.New(strings.Join(errstr, "\n")) + } else { + cmd.err <- nil + } +} + +// unregisterFromViewReq is the command to unregister to a view. Has no +// impact on the data collection for client that are pulling data from the +// library. +type unregisterFromViewReq struct { + views []string + done chan struct{} +} + +func (cmd *unregisterFromViewReq) handleCommand(w *worker) { + for _, name := range cmd.views { + vi, ok := w.views[name] + if !ok { + continue + } + + // Report pending data for this view before removing it. + w.reportView(vi, time.Now()) + + vi.unsubscribe() + if !vi.isSubscribed() { + // this was the last subscription and view is not collecting anymore. + // The collected data can be cleared. + vi.clearRows() + } + delete(w.views, name) + } + cmd.done <- struct{}{} +} + +// retrieveDataReq is the command to retrieve data for a view. +type retrieveDataReq struct { + now time.Time + v string + c chan *retrieveDataResp +} + +type retrieveDataResp struct { + rows []*Row + err error +} + +func (cmd *retrieveDataReq) handleCommand(w *worker) { + vi, ok := w.views[cmd.v] + if !ok { + cmd.c <- &retrieveDataResp{ + nil, + fmt.Errorf("cannot retrieve data; view %q is not registered", cmd.v), + } + return + } + + if !vi.isSubscribed() { + cmd.c <- &retrieveDataResp{ + nil, + fmt.Errorf("cannot retrieve data; view %q has no subscriptions or collection is not forcibly started", cmd.v), + } + return + } + cmd.c <- &retrieveDataResp{ + vi.collectedRows(), + nil, + } +} + +// recordReq is the command to record data related to multiple measures +// at once. +type recordReq struct { + tm *tag.Map + ms []stats.Measurement + attachments map[string]string + t time.Time +} + +func (cmd *recordReq) handleCommand(w *worker) { + for _, m := range cmd.ms { + if (m == stats.Measurement{}) { // not registered + continue + } + ref := w.getMeasureRef(m.Measure().Name()) + for v := range ref.views { + e := &exemplar.Exemplar{ + Value: m.Value(), + Timestamp: cmd.t, + Attachments: cmd.attachments, + } + v.addSample(cmd.tm, e) + } + } +} + +// setReportingPeriodReq is the command to modify the duration between +// reporting the collected data to the registered clients. +type setReportingPeriodReq struct { + d time.Duration + c chan bool +} + +func (cmd *setReportingPeriodReq) handleCommand(w *worker) { + w.timer.Stop() + if cmd.d <= 0 { + w.timer = time.NewTicker(defaultReportingDuration) + } else { + w.timer = time.NewTicker(cmd.d) + } + cmd.c <- true +} diff --git a/vendor/go.opencensus.io/stats/view/worker_test.go b/vendor/go.opencensus.io/stats/view/worker_test.go new file mode 100644 index 0000000000..d430146481 --- /dev/null +++ b/vendor/go.opencensus.io/stats/view/worker_test.go @@ -0,0 +1,435 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package view + +import ( + "context" + "errors" + "sync" + "testing" + "time" + + "go.opencensus.io/stats" + "go.opencensus.io/tag" +) + +func Test_Worker_ViewRegistration(t *testing.T) { + someError := errors.New("some error") + + sc1 := make(chan *Data) + + type registration struct { + c chan *Data + vID string + err error + } + type testCase struct { + label string + registrations []registration + } + tcs := []testCase{ + { + "register v1ID", + []registration{ + { + sc1, + "v1ID", + nil, + }, + }, + }, + { + "register v1ID+v2ID", + []registration{ + { + sc1, + "v1ID", + nil, + }, + }, + }, + { + "register to v1ID; ??? to v1ID and view with same ID", + []registration{ + { + sc1, + "v1ID", + nil, + }, + { + sc1, + "v1SameNameID", + someError, + }, + }, + }, + } + + mf1 := stats.Float64("MF1/Test_Worker_ViewSubscription", "desc MF1", "unit") + mf2 := stats.Float64("MF2/Test_Worker_ViewSubscription", "desc MF2", "unit") + + for _, tc := range tcs { + t.Run(tc.label, func(t *testing.T) { + restart() + + views := map[string]*View{ + "v1ID": { + Name: "VF1", + Measure: mf1, + Aggregation: Count(), + }, + "v1SameNameID": { + Name: "VF1", + Description: "desc duplicate name VF1", + Measure: mf1, + Aggregation: Sum(), + }, + "v2ID": { + Name: "VF2", + Measure: mf2, + Aggregation: Count(), + }, + "vNilID": nil, + } + + for _, r := range tc.registrations { + v := views[r.vID] + err := Register(v) + if (err != nil) != (r.err != nil) { + t.Errorf("%v: Register() = %v, want %v", tc.label, err, r.err) + } + } + }) + } +} + +func Test_Worker_RecordFloat64(t *testing.T) { + restart() + + someError := errors.New("some error") + m := stats.Float64("Test_Worker_RecordFloat64/MF1", "desc MF1", "unit") + + k1, _ := tag.NewKey("k1") + k2, _ := tag.NewKey("k2") + ctx, err := tag.New(context.Background(), + tag.Insert(k1, "v1"), + tag.Insert(k2, "v2"), + ) + if err != nil { + t.Fatal(err) + } + + v1 := &View{"VF1", "desc VF1", []tag.Key{k1, k2}, m, Count()} + v2 := &View{"VF2", "desc VF2", []tag.Key{k1, k2}, m, Count()} + + type want struct { + v *View + rows []*Row + err error + } + type testCase struct { + label string + registrations []*View + records []float64 + wants []want + } + + tcs := []testCase{ + { + label: "0", + registrations: []*View{}, + records: []float64{1, 1}, + wants: []want{{v1, nil, someError}, {v2, nil, someError}}, + }, + { + label: "1", + registrations: []*View{v1}, + records: []float64{1, 1}, + wants: []want{ + { + v1, + []*Row{ + { + []tag.Tag{{Key: k1, Value: "v1"}, {Key: k2, Value: "v2"}}, + &CountData{Value: 2}, + }, + }, + nil, + }, + {v2, nil, someError}, + }, + }, + { + label: "2", + registrations: []*View{v1, v2}, + records: []float64{1, 1}, + wants: []want{ + { + v1, + []*Row{ + { + []tag.Tag{{Key: k1, Value: "v1"}, {Key: k2, Value: "v2"}}, + &CountData{Value: 2}, + }, + }, + nil, + }, + { + v2, + []*Row{ + { + []tag.Tag{{Key: k1, Value: "v1"}, {Key: k2, Value: "v2"}}, + &CountData{Value: 2}, + }, + }, + nil, + }, + }, + }, + } + + for _, tc := range tcs { + for _, v := range tc.registrations { + if err := Register(v); err != nil { + t.Fatalf("%v: Register(%v) = %v; want no errors", tc.label, v.Name, err) + } + } + + for _, value := range tc.records { + stats.Record(ctx, m.M(value)) + } + + for _, w := range tc.wants { + gotRows, err := RetrieveData(w.v.Name) + if (err != nil) != (w.err != nil) { + t.Fatalf("%s: RetrieveData(%v) = %v; want error = %v", tc.label, w.v.Name, err, w.err) + } + for _, got := range gotRows { + if !containsRow(w.rows, got) { + t.Errorf("%s: got row %#v; want none", tc.label, got) + break + } + } + for _, want := range w.rows { + if !containsRow(gotRows, want) { + t.Errorf("%s: got none; want %#v'", tc.label, want) + break + } + } + } + + // Cleaning up. + Unregister(tc.registrations...) + } +} + +func TestReportUsage(t *testing.T) { + ctx := context.Background() + + m := stats.Int64("measure", "desc", "unit") + + tests := []struct { + name string + view *View + wantMaxCount int64 + }{ + { + name: "cum", + view: &View{Name: "cum1", Measure: m, Aggregation: Count()}, + wantMaxCount: 8, + }, + { + name: "cum2", + view: &View{Name: "cum1", Measure: m, Aggregation: Count()}, + wantMaxCount: 8, + }, + } + + for _, tt := range tests { + restart() + SetReportingPeriod(25 * time.Millisecond) + + if err := Register(tt.view); err != nil { + t.Fatalf("%v: cannot register: %v", tt.name, err) + } + + e := &countExporter{} + RegisterExporter(e) + + stats.Record(ctx, m.M(1)) + stats.Record(ctx, m.M(1)) + stats.Record(ctx, m.M(1)) + stats.Record(ctx, m.M(1)) + + time.Sleep(50 * time.Millisecond) + + stats.Record(ctx, m.M(1)) + stats.Record(ctx, m.M(1)) + stats.Record(ctx, m.M(1)) + stats.Record(ctx, m.M(1)) + + time.Sleep(50 * time.Millisecond) + + e.Lock() + count := e.count + e.Unlock() + if got, want := count, tt.wantMaxCount; got > want { + t.Errorf("%v: got count data = %v; want at most %v", tt.name, got, want) + } + } + +} + +func Test_SetReportingPeriodReqNeverBlocks(t *testing.T) { + t.Parallel() + + worker := newWorker() + durations := []time.Duration{-1, 0, 10, 100 * time.Millisecond} + for i, duration := range durations { + ackChan := make(chan bool, 1) + cmd := &setReportingPeriodReq{c: ackChan, d: duration} + cmd.handleCommand(worker) + + select { + case <-ackChan: + case <-time.After(500 * time.Millisecond): // Arbitrarily using 500ms as the timeout duration. + t.Errorf("#%d: duration %v blocks", i, duration) + } + } +} + +func TestWorkerStarttime(t *testing.T) { + restart() + + ctx := context.Background() + m := stats.Int64("measure/TestWorkerStarttime", "desc", "unit") + v := &View{ + Name: "testview", + Measure: m, + Aggregation: Count(), + } + + SetReportingPeriod(25 * time.Millisecond) + if err := Register(v); err != nil { + t.Fatalf("cannot register to %v: %v", v.Name, err) + } + + e := &vdExporter{} + RegisterExporter(e) + defer UnregisterExporter(e) + + stats.Record(ctx, m.M(1)) + stats.Record(ctx, m.M(1)) + stats.Record(ctx, m.M(1)) + stats.Record(ctx, m.M(1)) + + time.Sleep(50 * time.Millisecond) + + stats.Record(ctx, m.M(1)) + stats.Record(ctx, m.M(1)) + stats.Record(ctx, m.M(1)) + stats.Record(ctx, m.M(1)) + + time.Sleep(50 * time.Millisecond) + + e.Lock() + if len(e.vds) == 0 { + t.Fatal("Got no view data; want at least one") + } + + var start time.Time + for _, vd := range e.vds { + if start.IsZero() { + start = vd.Start + } + if !vd.Start.Equal(start) { + t.Errorf("Cumulative view data start time = %v; want %v", vd.Start, start) + } + } + e.Unlock() +} + +func TestUnregisterReportsUsage(t *testing.T) { + restart() + ctx := context.Background() + + m1 := stats.Int64("measure", "desc", "unit") + view1 := &View{Name: "count", Measure: m1, Aggregation: Count()} + m2 := stats.Int64("measure2", "desc", "unit") + view2 := &View{Name: "count2", Measure: m2, Aggregation: Count()} + + SetReportingPeriod(time.Hour) + + if err := Register(view1, view2); err != nil { + t.Fatalf("cannot register: %v", err) + } + + e := &countExporter{} + RegisterExporter(e) + + stats.Record(ctx, m1.M(1)) + stats.Record(ctx, m2.M(1)) + stats.Record(ctx, m2.M(1)) + + Unregister(view2) + + // Unregister should only flush view2, so expect the count of 2. + want := int64(2) + + e.Lock() + got := e.totalCount + e.Unlock() + if got != want { + t.Errorf("got count data = %v; want %v", got, want) + } +} + +type countExporter struct { + sync.Mutex + count int64 + totalCount int64 +} + +func (e *countExporter) ExportView(vd *Data) { + if len(vd.Rows) == 0 { + return + } + d := vd.Rows[0].Data.(*CountData) + + e.Lock() + defer e.Unlock() + e.count = d.Value + e.totalCount += d.Value +} + +type vdExporter struct { + sync.Mutex + vds []*Data +} + +func (e *vdExporter) ExportView(vd *Data) { + e.Lock() + defer e.Unlock() + + e.vds = append(e.vds, vd) +} + +// restart stops the current processors and creates a new one. +func restart() { + defaultWorker.stop() + defaultWorker = newWorker() + go defaultWorker.start() +} diff --git a/vendor/go.opencensus.io/tag/context.go b/vendor/go.opencensus.io/tag/context.go new file mode 100644 index 0000000000..dcc13f4987 --- /dev/null +++ b/vendor/go.opencensus.io/tag/context.go @@ -0,0 +1,67 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package tag + +import ( + "context" + + "go.opencensus.io/exemplar" +) + +// FromContext returns the tag map stored in the context. +func FromContext(ctx context.Context) *Map { + // The returned tag map shouldn't be mutated. + ts := ctx.Value(mapCtxKey) + if ts == nil { + return nil + } + return ts.(*Map) +} + +// NewContext creates a new context with the given tag map. +// To propagate a tag map to downstream methods and downstream RPCs, add a tag map +// to the current context. NewContext will return a copy of the current context, +// and put the tag map into the returned one. +// If there is already a tag map in the current context, it will be replaced with m. +func NewContext(ctx context.Context, m *Map) context.Context { + return context.WithValue(ctx, mapCtxKey, m) +} + +type ctxKey struct{} + +var mapCtxKey = ctxKey{} + +func init() { + exemplar.RegisterAttachmentExtractor(extractTagsAttachments) +} + +func extractTagsAttachments(ctx context.Context, a exemplar.Attachments) exemplar.Attachments { + m := FromContext(ctx) + if m == nil { + return a + } + if len(m.m) == 0 { + return a + } + if a == nil { + a = make(map[string]string) + } + + for k, v := range m.m { + a[exemplar.KeyPrefixTag+k.Name()] = v + } + return a +} diff --git a/vendor/go.opencensus.io/tag/context_test.go b/vendor/go.opencensus.io/tag/context_test.go new file mode 100644 index 0000000000..e85b1c40c3 --- /dev/null +++ b/vendor/go.opencensus.io/tag/context_test.go @@ -0,0 +1,44 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package tag + +import ( + "context" + "testing" +) + +func TestExtractTagsAttachment(t *testing.T) { + // We can't depend on the stats of view package without creating a + // dependency cycle. + + var m map[string]string + ctx := context.Background() + + res := extractTagsAttachments(ctx, m) + if res != nil { + t.Fatalf("res = %v; want nil", res) + } + + k, _ := NewKey("test") + ctx, _ = New(ctx, Insert(k, "test123")) + res = extractTagsAttachments(ctx, m) + if res == nil { + t.Fatal("res = nil") + } + if got, want := res["tag:test"], "test123"; got != want { + t.Fatalf("res[Tags:test] = %v; want %v", got, want) + } +} diff --git a/vendor/go.opencensus.io/tag/doc.go b/vendor/go.opencensus.io/tag/doc.go new file mode 100644 index 0000000000..da16b74e4d --- /dev/null +++ b/vendor/go.opencensus.io/tag/doc.go @@ -0,0 +1,26 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/* +Package tag contains OpenCensus tags. + +Tags are key-value pairs. Tags provide additional cardinality to +the OpenCensus instrumentation data. + +Tags can be propagated on the wire and in the same +process via context.Context. Encode and Decode should be +used to represent tags into their binary propagation form. +*/ +package tag // import "go.opencensus.io/tag" diff --git a/vendor/go.opencensus.io/tag/example_test.go b/vendor/go.opencensus.io/tag/example_test.go new file mode 100644 index 0000000000..fe0c5d9e96 --- /dev/null +++ b/vendor/go.opencensus.io/tag/example_test.go @@ -0,0 +1,97 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package tag_test + +import ( + "context" + "log" + + "go.opencensus.io/tag" +) + +var ( + tagMap *tag.Map + ctx context.Context + key tag.Key +) + +func ExampleNewKey() { + // Get a key to represent user OS. + key, err := tag.NewKey("example.com/keys/user-os") + if err != nil { + log.Fatal(err) + } + _ = key // use key +} + +func ExampleNew() { + osKey, err := tag.NewKey("example.com/keys/user-os") + if err != nil { + log.Fatal(err) + } + userIDKey, err := tag.NewKey("example.com/keys/user-id") + if err != nil { + log.Fatal(err) + } + + ctx, err := tag.New(ctx, + tag.Insert(osKey, "macOS-10.12.5"), + tag.Upsert(userIDKey, "cde36753ed"), + ) + if err != nil { + log.Fatal(err) + } + + _ = ctx // use context +} + +func ExampleNew_replace() { + ctx, err := tag.New(ctx, + tag.Insert(key, "macOS-10.12.5"), + tag.Upsert(key, "macOS-10.12.7"), + ) + if err != nil { + log.Fatal(err) + } + + _ = ctx // use context +} + +func ExampleNewContext() { + // Propagate the tag map in the current context. + ctx := tag.NewContext(context.Background(), tagMap) + + _ = ctx // use context +} + +func ExampleFromContext() { + tagMap := tag.FromContext(ctx) + + _ = tagMap // use the tag map +} + +func ExampleDo() { + ctx, err := tag.New(ctx, + tag.Insert(key, "macOS-10.12.5"), + tag.Upsert(key, "macOS-10.12.7"), + ) + if err != nil { + log.Fatal(err) + } + tag.Do(ctx, func(ctx context.Context) { + _ = ctx // use context + }) +} diff --git a/vendor/go.opencensus.io/tag/key.go b/vendor/go.opencensus.io/tag/key.go new file mode 100644 index 0000000000..ebbed95000 --- /dev/null +++ b/vendor/go.opencensus.io/tag/key.go @@ -0,0 +1,35 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package tag + +// Key represents a tag key. +type Key struct { + name string +} + +// NewKey creates or retrieves a string key identified by name. +// Calling NewKey consequently with the same name returns the same key. +func NewKey(name string) (Key, error) { + if !checkKeyName(name) { + return Key{}, errInvalidKeyName + } + return Key{name: name}, nil +} + +// Name returns the name of the key. +func (k Key) Name() string { + return k.name +} diff --git a/vendor/go.opencensus.io/tag/map.go b/vendor/go.opencensus.io/tag/map.go new file mode 100644 index 0000000000..5b72ba6ad3 --- /dev/null +++ b/vendor/go.opencensus.io/tag/map.go @@ -0,0 +1,197 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package tag + +import ( + "bytes" + "context" + "fmt" + "sort" +) + +// Tag is a key value pair that can be propagated on wire. +type Tag struct { + Key Key + Value string +} + +// Map is a map of tags. Use New to create a context containing +// a new Map. +type Map struct { + m map[Key]string +} + +// Value returns the value for the key if a value for the key exists. +func (m *Map) Value(k Key) (string, bool) { + if m == nil { + return "", false + } + v, ok := m.m[k] + return v, ok +} + +func (m *Map) String() string { + if m == nil { + return "nil" + } + keys := make([]Key, 0, len(m.m)) + for k := range m.m { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { return keys[i].Name() < keys[j].Name() }) + + var buffer bytes.Buffer + buffer.WriteString("{ ") + for _, k := range keys { + buffer.WriteString(fmt.Sprintf("{%v %v}", k.name, m.m[k])) + } + buffer.WriteString(" }") + return buffer.String() +} + +func (m *Map) insert(k Key, v string) { + if _, ok := m.m[k]; ok { + return + } + m.m[k] = v +} + +func (m *Map) update(k Key, v string) { + if _, ok := m.m[k]; ok { + m.m[k] = v + } +} + +func (m *Map) upsert(k Key, v string) { + m.m[k] = v +} + +func (m *Map) delete(k Key) { + delete(m.m, k) +} + +func newMap() *Map { + return &Map{m: make(map[Key]string)} +} + +// Mutator modifies a tag map. +type Mutator interface { + Mutate(t *Map) (*Map, error) +} + +// Insert returns a mutator that inserts a +// value associated with k. If k already exists in the tag map, +// mutator doesn't update the value. +func Insert(k Key, v string) Mutator { + return &mutator{ + fn: func(m *Map) (*Map, error) { + if !checkValue(v) { + return nil, errInvalidValue + } + m.insert(k, v) + return m, nil + }, + } +} + +// Update returns a mutator that updates the +// value of the tag associated with k with v. If k doesn't +// exists in the tag map, the mutator doesn't insert the value. +func Update(k Key, v string) Mutator { + return &mutator{ + fn: func(m *Map) (*Map, error) { + if !checkValue(v) { + return nil, errInvalidValue + } + m.update(k, v) + return m, nil + }, + } +} + +// Upsert returns a mutator that upserts the +// value of the tag associated with k with v. It inserts the +// value if k doesn't exist already. It mutates the value +// if k already exists. +func Upsert(k Key, v string) Mutator { + return &mutator{ + fn: func(m *Map) (*Map, error) { + if !checkValue(v) { + return nil, errInvalidValue + } + m.upsert(k, v) + return m, nil + }, + } +} + +// Delete returns a mutator that deletes +// the value associated with k. +func Delete(k Key) Mutator { + return &mutator{ + fn: func(m *Map) (*Map, error) { + m.delete(k) + return m, nil + }, + } +} + +// New returns a new context that contains a tag map +// originated from the incoming context and modified +// with the provided mutators. +func New(ctx context.Context, mutator ...Mutator) (context.Context, error) { + m := newMap() + orig := FromContext(ctx) + if orig != nil { + for k, v := range orig.m { + if !checkKeyName(k.Name()) { + return ctx, fmt.Errorf("key:%q: %v", k, errInvalidKeyName) + } + if !checkValue(v) { + return ctx, fmt.Errorf("key:%q value:%q: %v", k.Name(), v, errInvalidValue) + } + m.insert(k, v) + } + } + var err error + for _, mod := range mutator { + m, err = mod.Mutate(m) + if err != nil { + return ctx, err + } + } + return NewContext(ctx, m), nil +} + +// Do is similar to pprof.Do: a convenience for installing the tags +// from the context as Go profiler labels. This allows you to +// correlated runtime profiling with stats. +// +// It converts the key/values from the given map to Go profiler labels +// and calls pprof.Do. +// +// Do is going to do nothing if your Go version is below 1.9. +func Do(ctx context.Context, f func(ctx context.Context)) { + do(ctx, f) +} + +type mutator struct { + fn func(t *Map) (*Map, error) +} + +func (m *mutator) Mutate(t *Map) (*Map, error) { + return m.fn(t) +} diff --git a/vendor/go.opencensus.io/tag/map_codec.go b/vendor/go.opencensus.io/tag/map_codec.go new file mode 100644 index 0000000000..3e998950c3 --- /dev/null +++ b/vendor/go.opencensus.io/tag/map_codec.go @@ -0,0 +1,234 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package tag + +import ( + "encoding/binary" + "fmt" +) + +// KeyType defines the types of keys allowed. Currently only keyTypeString is +// supported. +type keyType byte + +const ( + keyTypeString keyType = iota + keyTypeInt64 + keyTypeTrue + keyTypeFalse + + tagsVersionID = byte(0) +) + +type encoderGRPC struct { + buf []byte + writeIdx, readIdx int +} + +// writeKeyString writes the fieldID '0' followed by the key string and value +// string. +func (eg *encoderGRPC) writeTagString(k, v string) { + eg.writeByte(byte(keyTypeString)) + eg.writeStringWithVarintLen(k) + eg.writeStringWithVarintLen(v) +} + +func (eg *encoderGRPC) writeTagUint64(k string, i uint64) { + eg.writeByte(byte(keyTypeInt64)) + eg.writeStringWithVarintLen(k) + eg.writeUint64(i) +} + +func (eg *encoderGRPC) writeTagTrue(k string) { + eg.writeByte(byte(keyTypeTrue)) + eg.writeStringWithVarintLen(k) +} + +func (eg *encoderGRPC) writeTagFalse(k string) { + eg.writeByte(byte(keyTypeFalse)) + eg.writeStringWithVarintLen(k) +} + +func (eg *encoderGRPC) writeBytesWithVarintLen(bytes []byte) { + length := len(bytes) + + eg.growIfRequired(binary.MaxVarintLen64 + length) + eg.writeIdx += binary.PutUvarint(eg.buf[eg.writeIdx:], uint64(length)) + copy(eg.buf[eg.writeIdx:], bytes) + eg.writeIdx += length +} + +func (eg *encoderGRPC) writeStringWithVarintLen(s string) { + length := len(s) + + eg.growIfRequired(binary.MaxVarintLen64 + length) + eg.writeIdx += binary.PutUvarint(eg.buf[eg.writeIdx:], uint64(length)) + copy(eg.buf[eg.writeIdx:], s) + eg.writeIdx += length +} + +func (eg *encoderGRPC) writeByte(v byte) { + eg.growIfRequired(1) + eg.buf[eg.writeIdx] = v + eg.writeIdx++ +} + +func (eg *encoderGRPC) writeUint32(i uint32) { + eg.growIfRequired(4) + binary.LittleEndian.PutUint32(eg.buf[eg.writeIdx:], i) + eg.writeIdx += 4 +} + +func (eg *encoderGRPC) writeUint64(i uint64) { + eg.growIfRequired(8) + binary.LittleEndian.PutUint64(eg.buf[eg.writeIdx:], i) + eg.writeIdx += 8 +} + +func (eg *encoderGRPC) readByte() byte { + b := eg.buf[eg.readIdx] + eg.readIdx++ + return b +} + +func (eg *encoderGRPC) readUint32() uint32 { + i := binary.LittleEndian.Uint32(eg.buf[eg.readIdx:]) + eg.readIdx += 4 + return i +} + +func (eg *encoderGRPC) readUint64() uint64 { + i := binary.LittleEndian.Uint64(eg.buf[eg.readIdx:]) + eg.readIdx += 8 + return i +} + +func (eg *encoderGRPC) readBytesWithVarintLen() ([]byte, error) { + if eg.readEnded() { + return nil, fmt.Errorf("unexpected end while readBytesWithVarintLen '%x' starting at idx '%v'", eg.buf, eg.readIdx) + } + length, valueStart := binary.Uvarint(eg.buf[eg.readIdx:]) + if valueStart <= 0 { + return nil, fmt.Errorf("unexpected end while readBytesWithVarintLen '%x' starting at idx '%v'", eg.buf, eg.readIdx) + } + + valueStart += eg.readIdx + valueEnd := valueStart + int(length) + if valueEnd > len(eg.buf) { + return nil, fmt.Errorf("malformed encoding: length:%v, upper:%v, maxLength:%v", length, valueEnd, len(eg.buf)) + } + + eg.readIdx = valueEnd + return eg.buf[valueStart:valueEnd], nil +} + +func (eg *encoderGRPC) readStringWithVarintLen() (string, error) { + bytes, err := eg.readBytesWithVarintLen() + if err != nil { + return "", err + } + return string(bytes), nil +} + +func (eg *encoderGRPC) growIfRequired(expected int) { + if len(eg.buf)-eg.writeIdx < expected { + tmp := make([]byte, 2*(len(eg.buf)+1)+expected) + copy(tmp, eg.buf) + eg.buf = tmp + } +} + +func (eg *encoderGRPC) readEnded() bool { + return eg.readIdx >= len(eg.buf) +} + +func (eg *encoderGRPC) bytes() []byte { + return eg.buf[:eg.writeIdx] +} + +// Encode encodes the tag map into a []byte. It is useful to propagate +// the tag maps on wire in binary format. +func Encode(m *Map) []byte { + eg := &encoderGRPC{ + buf: make([]byte, len(m.m)), + } + eg.writeByte(byte(tagsVersionID)) + for k, v := range m.m { + eg.writeByte(byte(keyTypeString)) + eg.writeStringWithVarintLen(k.name) + eg.writeBytesWithVarintLen([]byte(v)) + } + return eg.bytes() +} + +// Decode decodes the given []byte into a tag map. +func Decode(bytes []byte) (*Map, error) { + ts := newMap() + err := DecodeEach(bytes, ts.upsert) + if err != nil { + // no partial failures + return nil, err + } + return ts, nil +} + +// DecodeEach decodes the given serialized tag map, calling handler for each +// tag key and value decoded. +func DecodeEach(bytes []byte, fn func(key Key, val string)) error { + eg := &encoderGRPC{ + buf: bytes, + } + if len(eg.buf) == 0 { + return nil + } + + version := eg.readByte() + if version > tagsVersionID { + return fmt.Errorf("cannot decode: unsupported version: %q; supports only up to: %q", version, tagsVersionID) + } + + for !eg.readEnded() { + typ := keyType(eg.readByte()) + + if typ != keyTypeString { + return fmt.Errorf("cannot decode: invalid key type: %q", typ) + } + + k, err := eg.readBytesWithVarintLen() + if err != nil { + return err + } + + v, err := eg.readBytesWithVarintLen() + if err != nil { + return err + } + + key, err := NewKey(string(k)) + if err != nil { + return err + } + val := string(v) + if !checkValue(val) { + return errInvalidValue + } + fn(key, val) + if err != nil { + return err + } + } + return nil +} diff --git a/vendor/go.opencensus.io/tag/map_codec_test.go b/vendor/go.opencensus.io/tag/map_codec_test.go new file mode 100644 index 0000000000..a5605e690b --- /dev/null +++ b/vendor/go.opencensus.io/tag/map_codec_test.go @@ -0,0 +1,154 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package tag + +import ( + "context" + "reflect" + "sort" + "testing" +) + +func TestEncodeDecode(t *testing.T) { + k1, _ := NewKey("k1") + k2, _ := NewKey("k2") + k3, _ := NewKey("k3 is very weird <>.,?/'\";:`~!@#$%^&*()_-+={[}]|\\") + k4, _ := NewKey("k4") + + type keyValue struct { + k Key + v string + } + + testCases := []struct { + label string + pairs []keyValue + }{ + { + "0", + []keyValue{}, + }, + { + "1", + []keyValue{ + {k1, "v1"}, + }, + }, + { + "2", + []keyValue{ + {k1, "v1"}, + {k2, "v2"}, + }, + }, + { + "3", + []keyValue{ + {k1, "v1"}, + {k2, "v2"}, + {k3, "v3"}, + }, + }, + { + "4", + []keyValue{ + {k1, "v1"}, + {k2, "v2"}, + {k3, "v3"}, + {k4, "v4 is very weird <>.,?/'\";:`~!@#$%^&*()_-+={[}]|\\"}, + }, + }, + } + + for _, tc := range testCases { + mods := make([]Mutator, len(tc.pairs)) + for i, pair := range tc.pairs { + mods[i] = Upsert(pair.k, pair.v) + } + ctx, err := New(context.Background(), mods...) + if err != nil { + t.Errorf("%v: New = %v", tc.label, err) + } + + encoded := Encode(FromContext(ctx)) + decoded, err := Decode(encoded) + if err != nil { + t.Errorf("%v: decoding encoded tag map failed: %v", tc.label, err) + } + + got := make([]keyValue, 0) + for k, v := range decoded.m { + got = append(got, keyValue{k, string(v)}) + } + want := tc.pairs + + sort.Slice(got, func(i, j int) bool { return got[i].k.name < got[j].k.name }) + sort.Slice(want, func(i, j int) bool { return got[i].k.name < got[j].k.name }) + + if !reflect.DeepEqual(got, tc.pairs) { + t.Errorf("%v: decoded tag map = %#v; want %#v", tc.label, got, want) + } + } +} + +func TestDecode(t *testing.T) { + k1, _ := NewKey("k1") + ctx, _ := New(context.Background(), Insert(k1, "v1")) + + tests := []struct { + name string + bytes []byte + want *Map + wantErr bool + }{ + { + name: "valid", + bytes: []byte{0, 0, 2, 107, 49, 2, 118, 49}, + want: FromContext(ctx), + wantErr: false, + }, + { + name: "non-ascii key", + bytes: []byte{0, 0, 2, 107, 49, 2, 118, 49, 0, 2, 107, 25, 2, 118, 49}, + want: nil, + wantErr: true, + }, + { + name: "non-ascii value", + bytes: []byte{0, 0, 2, 107, 49, 2, 118, 49, 0, 2, 107, 50, 2, 118, 25}, + want: nil, + wantErr: true, + }, + { + name: "long value", + bytes: []byte{0, 0, 2, 107, 49, 2, 118, 49, 0, 2, 107, 50, 172, 2, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97}, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Decode(tt.bytes) + if (err != nil) != tt.wantErr { + t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Decode() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/vendor/go.opencensus.io/tag/map_test.go b/vendor/go.opencensus.io/tag/map_test.go new file mode 100644 index 0000000000..855b747a4d --- /dev/null +++ b/vendor/go.opencensus.io/tag/map_test.go @@ -0,0 +1,222 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package tag + +import ( + "context" + "fmt" + "reflect" + "strings" + "testing" +) + +func TestContext(t *testing.T) { + k1, _ := NewKey("k1") + k2, _ := NewKey("k2") + + ctx := context.Background() + ctx, _ = New(ctx, + Insert(k1, "v1"), + Insert(k2, "v2"), + ) + got := FromContext(ctx) + want := newMap() + want.insert(k1, "v1") + want.insert(k2, "v2") + + if !reflect.DeepEqual(got, want) { + t.Errorf("Map = %#v; want %#v", got, want) + } +} + +func TestDo(t *testing.T) { + k1, _ := NewKey("k1") + k2, _ := NewKey("k2") + ctx := context.Background() + ctx, _ = New(ctx, + Insert(k1, "v1"), + Insert(k2, "v2"), + ) + got := FromContext(ctx) + want := newMap() + want.insert(k1, "v1") + want.insert(k2, "v2") + Do(ctx, func(ctx context.Context) { + got = FromContext(ctx) + }) + if !reflect.DeepEqual(got, want) { + t.Errorf("Map = %#v; want %#v", got, want) + } +} + +func TestNewMap(t *testing.T) { + k1, _ := NewKey("k1") + k2, _ := NewKey("k2") + k3, _ := NewKey("k3") + k4, _ := NewKey("k4") + k5, _ := NewKey("k5") + + initial := makeTestTagMap(5) + + tests := []struct { + name string + initial *Map + mods []Mutator + want *Map + }{ + { + name: "from empty; insert", + initial: nil, + mods: []Mutator{ + Insert(k5, "v5"), + }, + want: makeTestTagMap(2, 4, 5), + }, + { + name: "from empty; insert existing", + initial: nil, + mods: []Mutator{ + Insert(k1, "v1"), + }, + want: makeTestTagMap(1, 2, 4), + }, + { + name: "from empty; update", + initial: nil, + mods: []Mutator{ + Update(k1, "v1"), + }, + want: makeTestTagMap(2, 4), + }, + { + name: "from empty; update unexisting", + initial: nil, + mods: []Mutator{ + Update(k5, "v5"), + }, + want: makeTestTagMap(2, 4), + }, + { + name: "from existing; upsert", + initial: initial, + mods: []Mutator{ + Upsert(k5, "v5"), + }, + want: makeTestTagMap(2, 4, 5), + }, + { + name: "from existing; delete", + initial: initial, + mods: []Mutator{ + Delete(k2), + }, + want: makeTestTagMap(4, 5), + }, + { + name: "from empty; invalid", + initial: nil, + mods: []Mutator{ + Insert(k5, "v\x19"), + Upsert(k5, "v\x19"), + Update(k5, "v\x19"), + }, + want: nil, + }, + { + name: "from empty; no partial", + initial: nil, + mods: []Mutator{ + Insert(k5, "v1"), + Update(k5, "v\x19"), + }, + want: nil, + }, + } + + for _, tt := range tests { + mods := []Mutator{ + Insert(k1, "v1"), + Insert(k2, "v2"), + Update(k3, "v3"), + Upsert(k4, "v4"), + Insert(k2, "v2"), + Delete(k1), + } + mods = append(mods, tt.mods...) + ctx := NewContext(context.Background(), tt.initial) + ctx, err := New(ctx, mods...) + if tt.want != nil && err != nil { + t.Errorf("%v: New = %v", tt.name, err) + } + + if got, want := FromContext(ctx), tt.want; !reflect.DeepEqual(got, want) { + t.Errorf("%v: got %v; want %v", tt.name, got, want) + } + } +} + +func TestNewValidation(t *testing.T) { + tests := []struct { + err string + seed *Map + }{ + // Key name validation in seed + {err: "invalid key", seed: &Map{m: map[Key]string{{name: ""}: "foo"}}}, + {err: "", seed: &Map{m: map[Key]string{{name: "key"}: "foo"}}}, + {err: "", seed: &Map{m: map[Key]string{{name: strings.Repeat("a", 255)}: "census"}}}, + {err: "invalid key", seed: &Map{m: map[Key]string{{name: strings.Repeat("a", 256)}: "census"}}}, + {err: "invalid key", seed: &Map{m: map[Key]string{{name: "Приве́т"}: "census"}}}, + + // Value validation + {err: "", seed: &Map{m: map[Key]string{{name: "key"}: ""}}}, + {err: "", seed: &Map{m: map[Key]string{{name: "key"}: strings.Repeat("a", 255)}}}, + {err: "invalid value", seed: &Map{m: map[Key]string{{name: "key"}: "Приве́т"}}}, + {err: "invalid value", seed: &Map{m: map[Key]string{{name: "key"}: strings.Repeat("a", 256)}}}, + } + + for i, tt := range tests { + ctx := NewContext(context.Background(), tt.seed) + ctx, err := New(ctx) + + if tt.err != "" { + if err == nil { + t.Errorf("#%d: got nil error; want %q", i, tt.err) + continue + } else if s, substr := err.Error(), tt.err; !strings.Contains(s, substr) { + t.Errorf("#%d:\ngot %q\nwant %q", i, s, substr) + } + continue + } + if err != nil { + t.Errorf("#%d: got %q want nil", i, err) + continue + } + m := FromContext(ctx) + if m == nil { + t.Errorf("#%d: got nil map", i) + continue + } + } +} + +func makeTestTagMap(ids ...int) *Map { + m := newMap() + for _, v := range ids { + k, _ := NewKey(fmt.Sprintf("k%d", v)) + m.m[k] = fmt.Sprintf("v%d", v) + } + return m +} diff --git a/vendor/go.opencensus.io/tag/profile_19.go b/vendor/go.opencensus.io/tag/profile_19.go new file mode 100644 index 0000000000..f81cd0b4a7 --- /dev/null +++ b/vendor/go.opencensus.io/tag/profile_19.go @@ -0,0 +1,31 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build go1.9 + +package tag + +import ( + "context" + "runtime/pprof" +) + +func do(ctx context.Context, f func(ctx context.Context)) { + m := FromContext(ctx) + keyvals := make([]string, 0, 2*len(m.m)) + for k, v := range m.m { + keyvals = append(keyvals, k.Name(), v) + } + pprof.Do(ctx, pprof.Labels(keyvals...), f) +} diff --git a/vendor/go.opencensus.io/tag/profile_not19.go b/vendor/go.opencensus.io/tag/profile_not19.go new file mode 100644 index 0000000000..83adbce56b --- /dev/null +++ b/vendor/go.opencensus.io/tag/profile_not19.go @@ -0,0 +1,23 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !go1.9 + +package tag + +import "context" + +func do(ctx context.Context, f func(ctx context.Context)) { + f(ctx) +} diff --git a/vendor/go.opencensus.io/tag/validate.go b/vendor/go.opencensus.io/tag/validate.go new file mode 100644 index 0000000000..0939fc6748 --- /dev/null +++ b/vendor/go.opencensus.io/tag/validate.go @@ -0,0 +1,56 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tag + +import "errors" + +const ( + maxKeyLength = 255 + + // valid are restricted to US-ASCII subset (range 0x20 (' ') to 0x7e ('~')). + validKeyValueMin = 32 + validKeyValueMax = 126 +) + +var ( + errInvalidKeyName = errors.New("invalid key name: only ASCII characters accepted; max length must be 255 characters") + errInvalidValue = errors.New("invalid value: only ASCII characters accepted; max length must be 255 characters") +) + +func checkKeyName(name string) bool { + if len(name) == 0 { + return false + } + if len(name) > maxKeyLength { + return false + } + return isASCII(name) +} + +func isASCII(s string) bool { + for _, c := range s { + if (c < validKeyValueMin) || (c > validKeyValueMax) { + return false + } + } + return true +} + +func checkValue(v string) bool { + if len(v) > maxKeyLength { + return false + } + return isASCII(v) +} diff --git a/vendor/go.opencensus.io/tag/validate_test.go b/vendor/go.opencensus.io/tag/validate_test.go new file mode 100644 index 0000000000..371a775c48 --- /dev/null +++ b/vendor/go.opencensus.io/tag/validate_test.go @@ -0,0 +1,110 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tag + +import ( + "strings" + "testing" +) + +func TestCheckKeyName(t *testing.T) { + tests := []struct { + name string + key string + wantOK bool + }{ + { + name: "valid", + key: "k1", + wantOK: true, + }, + { + name: "invalid i", + key: "k\x19", + wantOK: false, + }, + { + name: "invalid ii", + key: "k\x7f", + wantOK: false, + }, + { + name: "empty", + key: "", + wantOK: false, + }, + { + name: "whitespace", + key: " k ", + wantOK: true, + }, + { + name: "long", + key: strings.Repeat("a", 256), + wantOK: false, + }, + } + for _, tt := range tests { + ok := checkKeyName(tt.key) + if ok != tt.wantOK { + t.Errorf("%v: got %v; want %v", tt.name, ok, tt.wantOK) + } + } +} + +func TestCheckValue(t *testing.T) { + tests := []struct { + name string + value string + wantOK bool + }{ + { + name: "valid", + value: "v1", + wantOK: true, + }, + { + name: "invalid i", + value: "k\x19", + wantOK: false, + }, + { + name: "invalid ii", + value: "k\x7f", + wantOK: false, + }, + { + name: "empty", + value: "", + wantOK: true, + }, + { + name: "whitespace", + value: " v ", + wantOK: true, + }, + { + name: "long", + value: strings.Repeat("a", 256), + wantOK: false, + }, + } + for _, tt := range tests { + ok := checkValue(tt.value) + if ok != tt.wantOK { + t.Errorf("%v: got %v; want %v", tt.name, ok, tt.wantOK) + } + } +} diff --git a/vendor/go.opencensus.io/trace/basetypes.go b/vendor/go.opencensus.io/trace/basetypes.go new file mode 100644 index 0000000000..01f0f90831 --- /dev/null +++ b/vendor/go.opencensus.io/trace/basetypes.go @@ -0,0 +1,114 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "fmt" + "time" +) + +type ( + // TraceID is a 16-byte identifier for a set of spans. + TraceID [16]byte + + // SpanID is an 8-byte identifier for a single span. + SpanID [8]byte +) + +func (t TraceID) String() string { + return fmt.Sprintf("%02x", t[:]) +} + +func (s SpanID) String() string { + return fmt.Sprintf("%02x", s[:]) +} + +// Annotation represents a text annotation with a set of attributes and a timestamp. +type Annotation struct { + Time time.Time + Message string + Attributes map[string]interface{} +} + +// Attribute represents a key-value pair on a span, link or annotation. +// Construct with one of: BoolAttribute, Int64Attribute, or StringAttribute. +type Attribute struct { + key string + value interface{} +} + +// BoolAttribute returns a bool-valued attribute. +func BoolAttribute(key string, value bool) Attribute { + return Attribute{key: key, value: value} +} + +// Int64Attribute returns an int64-valued attribute. +func Int64Attribute(key string, value int64) Attribute { + return Attribute{key: key, value: value} +} + +// StringAttribute returns a string-valued attribute. +func StringAttribute(key string, value string) Attribute { + return Attribute{key: key, value: value} +} + +// LinkType specifies the relationship between the span that had the link +// added, and the linked span. +type LinkType int32 + +// LinkType values. +const ( + LinkTypeUnspecified LinkType = iota // The relationship of the two spans is unknown. + LinkTypeChild // The current span is a child of the linked span. + LinkTypeParent // The current span is the parent of the linked span. +) + +// Link represents a reference from one span to another span. +type Link struct { + TraceID TraceID + SpanID SpanID + Type LinkType + // Attributes is a set of attributes on the link. + Attributes map[string]interface{} +} + +// MessageEventType specifies the type of message event. +type MessageEventType int32 + +// MessageEventType values. +const ( + MessageEventTypeUnspecified MessageEventType = iota // Unknown event type. + MessageEventTypeSent // Indicates a sent RPC message. + MessageEventTypeRecv // Indicates a received RPC message. +) + +// MessageEvent represents an event describing a message sent or received on the network. +type MessageEvent struct { + Time time.Time + EventType MessageEventType + MessageID int64 + UncompressedByteSize int64 + CompressedByteSize int64 +} + +// Status is the status of a Span. +type Status struct { + // Code is a status code. Zero indicates success. + // + // If Code will be propagated to Google APIs, it ideally should be a value from + // https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto . + Code int32 + Message string +} diff --git a/vendor/go.opencensus.io/trace/benchmark_test.go b/vendor/go.opencensus.io/trace/benchmark_test.go new file mode 100644 index 0000000000..7e86d64d9c --- /dev/null +++ b/vendor/go.opencensus.io/trace/benchmark_test.go @@ -0,0 +1,105 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "context" + "testing" +) + +func BenchmarkStartEndSpan(b *testing.B) { + traceBenchmark(b, func(b *testing.B) { + ctx := context.Background() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, span := StartSpan(ctx, "/foo") + span.End() + } + }) +} + +func BenchmarkSpanWithAnnotations_3(b *testing.B) { + traceBenchmark(b, func(b *testing.B) { + ctx := context.Background() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, span := StartSpan(ctx, "/foo") + span.AddAttributes( + BoolAttribute("key1", false), + StringAttribute("key2", "hello"), + Int64Attribute("key3", 123), + ) + span.End() + } + }) +} + +func BenchmarkSpanWithAnnotations_6(b *testing.B) { + traceBenchmark(b, func(b *testing.B) { + ctx := context.Background() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, span := StartSpan(ctx, "/foo") + span.AddAttributes( + BoolAttribute("key1", false), + BoolAttribute("key2", true), + StringAttribute("key3", "hello"), + StringAttribute("key4", "hello"), + Int64Attribute("key5", 123), + Int64Attribute("key6", 456), + ) + span.End() + } + }) +} + +func BenchmarkTraceID_DotString(b *testing.B) { + traceBenchmark(b, func(b *testing.B) { + t := TraceID{0x0D, 0x0E, 0x0A, 0x0D, 0x0B, 0x0E, 0x0E, 0x0F, 0x0F, 0x0E, 0x0E, 0x0B, 0x0D, 0x0A, 0x0E, 0x0D} + want := "0d0e0a0d0b0e0e0f0f0e0e0b0d0a0e0d" + for i := 0; i < b.N; i++ { + if got := t.String(); got != want { + b.Fatalf("got = %q want = %q", got, want) + } + } + }) +} + +func BenchmarkSpanID_DotString(b *testing.B) { + traceBenchmark(b, func(b *testing.B) { + s := SpanID{0x0D, 0x0E, 0x0A, 0x0D, 0x0B, 0x0E, 0x0E, 0x0F} + want := "0d0e0a0d0b0e0e0f" + for i := 0; i < b.N; i++ { + if got := s.String(); got != want { + b.Fatalf("got = %q want = %q", got, want) + } + } + }) +} + +func traceBenchmark(b *testing.B, fn func(*testing.B)) { + b.Run("AlwaysSample", func(b *testing.B) { + b.ReportAllocs() + ApplyConfig(Config{DefaultSampler: AlwaysSample()}) + fn(b) + }) + b.Run("NeverSample", func(b *testing.B) { + b.ReportAllocs() + ApplyConfig(Config{DefaultSampler: NeverSample()}) + fn(b) + }) +} diff --git a/vendor/go.opencensus.io/trace/config.go b/vendor/go.opencensus.io/trace/config.go new file mode 100644 index 0000000000..0816892ea1 --- /dev/null +++ b/vendor/go.opencensus.io/trace/config.go @@ -0,0 +1,48 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "sync" + + "go.opencensus.io/trace/internal" +) + +// Config represents the global tracing configuration. +type Config struct { + // DefaultSampler is the default sampler used when creating new spans. + DefaultSampler Sampler + + // IDGenerator is for internal use only. + IDGenerator internal.IDGenerator +} + +var configWriteMu sync.Mutex + +// ApplyConfig applies changes to the global tracing configuration. +// +// Fields not provided in the given config are going to be preserved. +func ApplyConfig(cfg Config) { + configWriteMu.Lock() + defer configWriteMu.Unlock() + c := *config.Load().(*Config) + if cfg.DefaultSampler != nil { + c.DefaultSampler = cfg.DefaultSampler + } + if cfg.IDGenerator != nil { + c.IDGenerator = cfg.IDGenerator + } + config.Store(&c) +} diff --git a/vendor/go.opencensus.io/trace/config_test.go b/vendor/go.opencensus.io/trace/config_test.go new file mode 100644 index 0000000000..8495d8137c --- /dev/null +++ b/vendor/go.opencensus.io/trace/config_test.go @@ -0,0 +1,33 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "reflect" + "testing" +) + +func TestApplyZeroConfig(t *testing.T) { + cfg := config.Load().(*Config) + ApplyConfig(Config{}) + currentCfg := config.Load().(*Config) + + if got, want := reflect.ValueOf(currentCfg.DefaultSampler).Pointer(), reflect.ValueOf(cfg.DefaultSampler).Pointer(); got != want { + t.Fatalf("config.DefaultSampler = %#v; want %#v", got, want) + } + if got, want := currentCfg.IDGenerator, cfg.IDGenerator; got != want { + t.Fatalf("config.IDGenerator = %#v; want %#v", got, want) + } +} diff --git a/vendor/go.opencensus.io/trace/doc.go b/vendor/go.opencensus.io/trace/doc.go new file mode 100644 index 0000000000..04b1ee4f38 --- /dev/null +++ b/vendor/go.opencensus.io/trace/doc.go @@ -0,0 +1,53 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package trace contains support for OpenCensus distributed tracing. + +The following assumes a basic familiarity with OpenCensus concepts. +See http://opencensus.io + + +Exporting Traces + +To export collected tracing data, register at least one exporter. You can use +one of the provided exporters or write your own. + + trace.RegisterExporter(exporter) + +By default, traces will be sampled relatively rarely. To change the sampling +frequency for your entire program, call ApplyConfig. Use a ProbabilitySampler +to sample a subset of traces, or use AlwaysSample to collect a trace on every run: + + trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) + +Be careful about using trace.AlwaysSample in a production application with +significant traffic: a new trace will be started and exported for every request. + +Adding Spans to a Trace + +A trace consists of a tree of spans. In Go, the current span is carried in a +context.Context. + +It is common to want to capture all the activity of a function call in a span. For +this to work, the function must take a context.Context as a parameter. Add these two +lines to the top of the function: + + ctx, span := trace.StartSpan(ctx, "example.com/Run") + defer span.End() + +StartSpan will create a new top-level span if the context +doesn't contain another span, otherwise it will create a child span. +*/ +package trace // import "go.opencensus.io/trace" diff --git a/vendor/go.opencensus.io/trace/examples_test.go b/vendor/go.opencensus.io/trace/examples_test.go new file mode 100644 index 0000000000..41925d6c2d --- /dev/null +++ b/vendor/go.opencensus.io/trace/examples_test.go @@ -0,0 +1,41 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace_test + +import ( + "fmt" + + "go.opencensus.io/trace" + "golang.org/x/net/context" +) + +// This example shows how to use StartSpan and (*Span).End to capture +// a function execution in a Span. It assumes that the function +// has a context.Context argument. +func ExampleStartSpan() { + printEvens := func(ctx context.Context) { + ctx, span := trace.StartSpan(ctx, "my/package.Function") + defer span.End() + + for i := 0; i < 10; i++ { + if i%2 == 0 { + fmt.Printf("Even!\n") + } + } + } + + ctx := context.Background() + printEvens(ctx) +} diff --git a/vendor/go.opencensus.io/trace/exemplar.go b/vendor/go.opencensus.io/trace/exemplar.go new file mode 100644 index 0000000000..416d80590d --- /dev/null +++ b/vendor/go.opencensus.io/trace/exemplar.go @@ -0,0 +1,43 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "context" + "encoding/hex" + + "go.opencensus.io/exemplar" +) + +func init() { + exemplar.RegisterAttachmentExtractor(attachSpanContext) +} + +func attachSpanContext(ctx context.Context, a exemplar.Attachments) exemplar.Attachments { + span := FromContext(ctx) + if span == nil { + return a + } + sc := span.SpanContext() + if !sc.IsSampled() { + return a + } + if a == nil { + a = make(exemplar.Attachments) + } + a[exemplar.KeyTraceID] = hex.EncodeToString(sc.TraceID[:]) + a[exemplar.KeySpanID] = hex.EncodeToString(sc.SpanID[:]) + return a +} diff --git a/vendor/go.opencensus.io/trace/exemplar_test.go b/vendor/go.opencensus.io/trace/exemplar_test.go new file mode 100644 index 0000000000..de27631f99 --- /dev/null +++ b/vendor/go.opencensus.io/trace/exemplar_test.go @@ -0,0 +1,100 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace_test + +import ( + "context" + "testing" + + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/trace" +) + +func TestTraceExemplar(t *testing.T) { + m := stats.Float64("measure."+t.Name(), "", stats.UnitDimensionless) + v := &view.View{ + Measure: m, + Aggregation: view.Distribution(0, 1, 2, 3), + } + view.Register(v) + ctx := context.Background() + ctx, span := trace.StartSpan(ctx, t.Name(), trace.WithSampler(trace.AlwaysSample())) + stats.Record(ctx, m.M(1.5)) + span.End() + + rows, err := view.RetrieveData(v.Name) + if err != nil { + t.Fatal(err) + } + if len(rows) == 0 { + t.Fatal("len(rows) = 0; want > 0") + } + dd := rows[0].Data.(*view.DistributionData) + if got := len(dd.ExemplarsPerBucket); got < 3 { + t.Fatalf("len(dd.ExemplarsPerBucket) = %d; want >= 2", got) + } + exemplar := dd.ExemplarsPerBucket[2] + if exemplar == nil { + t.Fatal("Expected exemplar") + } + if got, want := exemplar.Value, 1.5; got != want { + t.Fatalf("exemplar.Value = %v; got %v", got, want) + } + if _, ok := exemplar.Attachments["trace_id"]; !ok { + t.Fatalf("exemplar.Attachments = %v; want trace_id key", exemplar.Attachments) + } + if _, ok := exemplar.Attachments["span_id"]; !ok { + t.Fatalf("exemplar.Attachments = %v; want span_id key", exemplar.Attachments) + } +} + +func TestTraceExemplar_notSampled(t *testing.T) { + m := stats.Float64("measure."+t.Name(), "", stats.UnitDimensionless) + v := &view.View{ + Measure: m, + Aggregation: view.Distribution(0, 1, 2, 3), + } + view.Register(v) + ctx := context.Background() + ctx, span := trace.StartSpan(ctx, t.Name(), trace.WithSampler(trace.NeverSample())) + stats.Record(ctx, m.M(1.5)) + span.End() + + rows, err := view.RetrieveData(v.Name) + if err != nil { + t.Fatal(err) + } + if len(rows) == 0 { + t.Fatal("len(rows) = 0; want > 0") + } + dd := rows[0].Data.(*view.DistributionData) + if got := len(dd.ExemplarsPerBucket); got < 3 { + t.Fatalf("len(buckets) = %d; want >= 2", got) + } + exemplar := dd.ExemplarsPerBucket[2] + if exemplar == nil { + t.Fatal("Expected exemplar") + } + if got, want := exemplar.Value, 1.5; got != want { + t.Fatalf("exemplar.Value = %v; got %v", got, want) + } + if _, ok := exemplar.Attachments["trace_id"]; ok { + t.Fatalf("exemplar.Attachments = %v; want no trace_id", exemplar.Attachments) + } + if _, ok := exemplar.Attachments["span_id"]; ok { + t.Fatalf("exemplar.Attachments = %v; want span_id key", exemplar.Attachments) + } +} diff --git a/vendor/go.opencensus.io/trace/export.go b/vendor/go.opencensus.io/trace/export.go new file mode 100644 index 0000000000..77a8c73575 --- /dev/null +++ b/vendor/go.opencensus.io/trace/export.go @@ -0,0 +1,90 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "sync" + "sync/atomic" + "time" +) + +// Exporter is a type for functions that receive sampled trace spans. +// +// The ExportSpan method should be safe for concurrent use and should return +// quickly; if an Exporter takes a significant amount of time to process a +// SpanData, that work should be done on another goroutine. +// +// The SpanData should not be modified, but a pointer to it can be kept. +type Exporter interface { + ExportSpan(s *SpanData) +} + +type exportersMap map[Exporter]struct{} + +var ( + exporterMu sync.Mutex + exporters atomic.Value +) + +// RegisterExporter adds to the list of Exporters that will receive sampled +// trace spans. +// +// Binaries can register exporters, libraries shouldn't register exporters. +func RegisterExporter(e Exporter) { + exporterMu.Lock() + new := make(exportersMap) + if old, ok := exporters.Load().(exportersMap); ok { + for k, v := range old { + new[k] = v + } + } + new[e] = struct{}{} + exporters.Store(new) + exporterMu.Unlock() +} + +// UnregisterExporter removes from the list of Exporters the Exporter that was +// registered with the given name. +func UnregisterExporter(e Exporter) { + exporterMu.Lock() + new := make(exportersMap) + if old, ok := exporters.Load().(exportersMap); ok { + for k, v := range old { + new[k] = v + } + } + delete(new, e) + exporters.Store(new) + exporterMu.Unlock() +} + +// SpanData contains all the information collected by a Span. +type SpanData struct { + SpanContext + ParentSpanID SpanID + SpanKind int + Name string + StartTime time.Time + // The wall clock time of EndTime will be adjusted to always be offset + // from StartTime by the duration of the span. + EndTime time.Time + // The values of Attributes each have type string, bool, or int64. + Attributes map[string]interface{} + Annotations []Annotation + MessageEvents []MessageEvent + Status + Links []Link + HasRemoteParent bool +} diff --git a/vendor/go.opencensus.io/trace/internal/internal.go b/vendor/go.opencensus.io/trace/internal/internal.go new file mode 100644 index 0000000000..1c8b9b34b2 --- /dev/null +++ b/vendor/go.opencensus.io/trace/internal/internal.go @@ -0,0 +1,21 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package internal provides trace internals. +package internal + +type IDGenerator interface { + NewTraceID() [16]byte + NewSpanID() [8]byte +} diff --git a/vendor/go.opencensus.io/trace/propagation/propagation.go b/vendor/go.opencensus.io/trace/propagation/propagation.go new file mode 100644 index 0000000000..1eb190a96a --- /dev/null +++ b/vendor/go.opencensus.io/trace/propagation/propagation.go @@ -0,0 +1,108 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package propagation implements the binary trace context format. +package propagation // import "go.opencensus.io/trace/propagation" + +// TODO: link to external spec document. + +// BinaryFormat format: +// +// Binary value: +// version_id: 1 byte representing the version id. +// +// For version_id = 0: +// +// version_format: +// field_format: +// +// Fields: +// +// TraceId: (field_id = 0, len = 16, default = "0000000000000000") - 16-byte array representing the trace_id. +// SpanId: (field_id = 1, len = 8, default = "00000000") - 8-byte array representing the span_id. +// TraceOptions: (field_id = 2, len = 1, default = "0") - 1-byte array representing the trace_options. +// +// Fields MUST be encoded using the field id order (smaller to higher). +// +// Valid value example: +// +// {0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97, +// 98, 99, 100, 101, 102, 103, 104, 2, 1} +// +// version_id = 0; +// trace_id = {64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79} +// span_id = {97, 98, 99, 100, 101, 102, 103, 104}; +// trace_options = {1}; + +import ( + "net/http" + + "go.opencensus.io/trace" +) + +// Binary returns the binary format representation of a SpanContext. +// +// If sc is the zero value, Binary returns nil. +func Binary(sc trace.SpanContext) []byte { + if sc == (trace.SpanContext{}) { + return nil + } + var b [29]byte + copy(b[2:18], sc.TraceID[:]) + b[18] = 1 + copy(b[19:27], sc.SpanID[:]) + b[27] = 2 + b[28] = uint8(sc.TraceOptions) + return b[:] +} + +// FromBinary returns the SpanContext represented by b. +// +// If b has an unsupported version ID or contains no TraceID, FromBinary +// returns with ok==false. +func FromBinary(b []byte) (sc trace.SpanContext, ok bool) { + if len(b) == 0 || b[0] != 0 { + return trace.SpanContext{}, false + } + b = b[1:] + if len(b) >= 17 && b[0] == 0 { + copy(sc.TraceID[:], b[1:17]) + b = b[17:] + } else { + return trace.SpanContext{}, false + } + if len(b) >= 9 && b[0] == 1 { + copy(sc.SpanID[:], b[1:9]) + b = b[9:] + } + if len(b) >= 2 && b[0] == 2 { + sc.TraceOptions = trace.TraceOptions(b[1]) + } + return sc, true +} + +// HTTPFormat implementations propagate span contexts +// in HTTP requests. +// +// SpanContextFromRequest extracts a span context from incoming +// requests. +// +// SpanContextToRequest modifies the given request to include the given +// span context. +type HTTPFormat interface { + SpanContextFromRequest(req *http.Request) (sc trace.SpanContext, ok bool) + SpanContextToRequest(sc trace.SpanContext, req *http.Request) +} + +// TODO(jbd): Find a more representative but short name for HTTPFormat. diff --git a/vendor/go.opencensus.io/trace/propagation/propagation_test.go b/vendor/go.opencensus.io/trace/propagation/propagation_test.go new file mode 100644 index 0000000000..d7e14aaa3a --- /dev/null +++ b/vendor/go.opencensus.io/trace/propagation/propagation_test.go @@ -0,0 +1,150 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package propagation + +import ( + "bytes" + "fmt" + "testing" + + . "go.opencensus.io/trace" +) + +func TestBinary(t *testing.T) { + tid := TraceID{0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f} + sid := SpanID{0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68} + b := []byte{ + 0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97, 98, 99, 100, + 101, 102, 103, 104, 2, 1, + } + if b2 := Binary(SpanContext{ + TraceID: tid, + SpanID: sid, + TraceOptions: 1, + }); !bytes.Equal(b2, b) { + t.Errorf("Binary: got serialization %02x want %02x", b2, b) + } + + sc, ok := FromBinary(b) + if !ok { + t.Errorf("FromBinary: got ok==%t, want true", ok) + } + if got := sc.TraceID; got != tid { + t.Errorf("FromBinary: got trace ID %s want %s", got, tid) + } + if got := sc.SpanID; got != sid { + t.Errorf("FromBinary: got span ID %s want %s", got, sid) + } + + b[0] = 1 + sc, ok = FromBinary(b) + if ok { + t.Errorf("FromBinary: decoding bytes containing an unsupported version: got ok==%t want false", ok) + } + + b = []byte{0, 1, 97, 98, 99, 100, 101, 102, 103, 104, 2, 1} + sc, ok = FromBinary(b) + if ok { + t.Errorf("FromBinary: decoding bytes without a TraceID: got ok==%t want false", ok) + } + + if b := Binary(SpanContext{}); b != nil { + t.Errorf("Binary(SpanContext{}): got serialization %02x want nil", b) + } +} + +func TestFromBinary(t *testing.T) { + validData := []byte{0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97, 98, 99, 100, 101, 102, 103, 104, 2, 1} + tests := []struct { + name string + data []byte + wantTraceID TraceID + wantSpanID SpanID + wantOpts TraceOptions + wantOk bool + }{ + { + name: "nil data", + data: nil, + wantOk: false, + }, + { + name: "short data", + data: []byte{0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77}, + wantOk: false, + }, + { + name: "wrong field number", + data: []byte{0, 1, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77}, + wantOk: false, + }, + { + name: "valid data", + data: validData, + wantTraceID: TraceID{64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}, + wantSpanID: SpanID{97, 98, 99, 100, 101, 102, 103, 104}, + wantOpts: 1, + wantOk: true, + }, + } + for _, tt := range tests { + sc, gotOk := FromBinary(tt.data) + gotTraceID, gotSpanID, gotOpts := sc.TraceID, sc.SpanID, sc.TraceOptions + if gotTraceID != tt.wantTraceID { + t.Errorf("%s: Decode() gotTraceID = %v, want %v", tt.name, gotTraceID, tt.wantTraceID) + } + if gotSpanID != tt.wantSpanID { + t.Errorf("%s: Decode() gotSpanID = %v, want %v", tt.name, gotSpanID, tt.wantSpanID) + } + if gotOpts != tt.wantOpts { + t.Errorf("%s: Decode() gotOpts = %v, want %v", tt.name, gotOpts, tt.wantOpts) + } + if gotOk != tt.wantOk { + t.Errorf("%s: Decode() gotOk = %v, want %v", tt.name, gotOk, tt.wantOk) + } + } +} + +func BenchmarkBinary(b *testing.B) { + tid := TraceID{0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f} + sid := SpanID{0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68} + sc := SpanContext{ + TraceID: tid, + SpanID: sid, + } + var x byte + for i := 0; i < b.N; i++ { + bin := Binary(sc) + x += bin[0] + } + if x == 1 { + fmt.Println(x) // try to prevent optimizing-out + } +} + +func BenchmarkFromBinary(b *testing.B) { + bin := []byte{ + 0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97, 98, 99, 100, + 101, 102, 103, 104, 2, 1, + } + var x byte + for i := 0; i < b.N; i++ { + sc, _ := FromBinary(bin) + x += sc.TraceID[0] + } + if x == 1 { + fmt.Println(x) // try to prevent optimizing-out + } +} diff --git a/vendor/go.opencensus.io/trace/sampling.go b/vendor/go.opencensus.io/trace/sampling.go new file mode 100644 index 0000000000..71c10f9e3b --- /dev/null +++ b/vendor/go.opencensus.io/trace/sampling.go @@ -0,0 +1,75 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "encoding/binary" +) + +const defaultSamplingProbability = 1e-4 + +// Sampler decides whether a trace should be sampled and exported. +type Sampler func(SamplingParameters) SamplingDecision + +// SamplingParameters contains the values passed to a Sampler. +type SamplingParameters struct { + ParentContext SpanContext + TraceID TraceID + SpanID SpanID + Name string + HasRemoteParent bool +} + +// SamplingDecision is the value returned by a Sampler. +type SamplingDecision struct { + Sample bool +} + +// ProbabilitySampler returns a Sampler that samples a given fraction of traces. +// +// It also samples spans whose parents are sampled. +func ProbabilitySampler(fraction float64) Sampler { + if !(fraction >= 0) { + fraction = 0 + } else if fraction >= 1 { + return AlwaysSample() + } + + traceIDUpperBound := uint64(fraction * (1 << 63)) + return Sampler(func(p SamplingParameters) SamplingDecision { + if p.ParentContext.IsSampled() { + return SamplingDecision{Sample: true} + } + x := binary.BigEndian.Uint64(p.TraceID[0:8]) >> 1 + return SamplingDecision{Sample: x < traceIDUpperBound} + }) +} + +// AlwaysSample returns a Sampler that samples every trace. +// Be careful about using this sampler in a production application with +// significant traffic: a new trace will be started and exported for every +// request. +func AlwaysSample() Sampler { + return func(p SamplingParameters) SamplingDecision { + return SamplingDecision{Sample: true} + } +} + +// NeverSample returns a Sampler that samples no traces. +func NeverSample() Sampler { + return func(p SamplingParameters) SamplingDecision { + return SamplingDecision{Sample: false} + } +} diff --git a/vendor/go.opencensus.io/trace/spanbucket.go b/vendor/go.opencensus.io/trace/spanbucket.go new file mode 100644 index 0000000000..fbabad34c0 --- /dev/null +++ b/vendor/go.opencensus.io/trace/spanbucket.go @@ -0,0 +1,130 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "time" +) + +// samplePeriod is the minimum time between accepting spans in a single bucket. +const samplePeriod = time.Second + +// defaultLatencies contains the default latency bucket bounds. +// TODO: consider defaults, make configurable +var defaultLatencies = [...]time.Duration{ + 10 * time.Microsecond, + 100 * time.Microsecond, + time.Millisecond, + 10 * time.Millisecond, + 100 * time.Millisecond, + time.Second, + 10 * time.Second, + time.Minute, +} + +// bucket is a container for a set of spans for a particular error code or latency range. +type bucket struct { + nextTime time.Time // next time we can accept a span + buffer []*SpanData // circular buffer of spans + nextIndex int // location next SpanData should be placed in buffer + overflow bool // whether the circular buffer has wrapped around +} + +func makeBucket(bufferSize int) bucket { + return bucket{ + buffer: make([]*SpanData, bufferSize), + } +} + +// add adds a span to the bucket, if nextTime has been reached. +func (b *bucket) add(s *SpanData) { + if s.EndTime.Before(b.nextTime) { + return + } + if len(b.buffer) == 0 { + return + } + b.nextTime = s.EndTime.Add(samplePeriod) + b.buffer[b.nextIndex] = s + b.nextIndex++ + if b.nextIndex == len(b.buffer) { + b.nextIndex = 0 + b.overflow = true + } +} + +// size returns the number of spans in the bucket. +func (b *bucket) size() int { + if b.overflow { + return len(b.buffer) + } + return b.nextIndex +} + +// span returns the ith span in the bucket. +func (b *bucket) span(i int) *SpanData { + if !b.overflow { + return b.buffer[i] + } + if i < len(b.buffer)-b.nextIndex { + return b.buffer[b.nextIndex+i] + } + return b.buffer[b.nextIndex+i-len(b.buffer)] +} + +// resize changes the size of the bucket to n, keeping up to n existing spans. +func (b *bucket) resize(n int) { + cur := b.size() + newBuffer := make([]*SpanData, n) + if cur < n { + for i := 0; i < cur; i++ { + newBuffer[i] = b.span(i) + } + b.buffer = newBuffer + b.nextIndex = cur + b.overflow = false + return + } + for i := 0; i < n; i++ { + newBuffer[i] = b.span(i + cur - n) + } + b.buffer = newBuffer + b.nextIndex = 0 + b.overflow = true +} + +// latencyBucket returns the appropriate bucket number for a given latency. +func latencyBucket(latency time.Duration) int { + i := 0 + for i < len(defaultLatencies) && latency >= defaultLatencies[i] { + i++ + } + return i +} + +// latencyBucketBounds returns the lower and upper bounds for a latency bucket +// number. +// +// The lower bound is inclusive, the upper bound is exclusive (except for the +// last bucket.) +func latencyBucketBounds(index int) (lower time.Duration, upper time.Duration) { + if index == 0 { + return 0, defaultLatencies[index] + } + if index == len(defaultLatencies) { + return defaultLatencies[index-1], 1<<63 - 1 + } + return defaultLatencies[index-1], defaultLatencies[index] +} diff --git a/vendor/go.opencensus.io/trace/spanstore.go b/vendor/go.opencensus.io/trace/spanstore.go new file mode 100644 index 0000000000..c442d99021 --- /dev/null +++ b/vendor/go.opencensus.io/trace/spanstore.go @@ -0,0 +1,306 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "sync" + "time" + + "go.opencensus.io/internal" +) + +const ( + maxBucketSize = 100000 + defaultBucketSize = 10 +) + +var ( + ssmu sync.RWMutex // protects spanStores + spanStores = make(map[string]*spanStore) +) + +// This exists purely to avoid exposing internal methods used by z-Pages externally. +type internalOnly struct{} + +func init() { + //TODO(#412): remove + internal.Trace = &internalOnly{} +} + +// ReportActiveSpans returns the active spans for the given name. +func (i internalOnly) ReportActiveSpans(name string) []*SpanData { + s := spanStoreForName(name) + if s == nil { + return nil + } + var out []*SpanData + s.mu.Lock() + defer s.mu.Unlock() + for span := range s.active { + out = append(out, span.makeSpanData()) + } + return out +} + +// ReportSpansByError returns a sample of error spans. +// +// If code is nonzero, only spans with that status code are returned. +func (i internalOnly) ReportSpansByError(name string, code int32) []*SpanData { + s := spanStoreForName(name) + if s == nil { + return nil + } + var out []*SpanData + s.mu.Lock() + defer s.mu.Unlock() + if code != 0 { + if b, ok := s.errors[code]; ok { + for _, sd := range b.buffer { + if sd == nil { + break + } + out = append(out, sd) + } + } + } else { + for _, b := range s.errors { + for _, sd := range b.buffer { + if sd == nil { + break + } + out = append(out, sd) + } + } + } + return out +} + +// ConfigureBucketSizes sets the number of spans to keep per latency and error +// bucket for different span names. +func (i internalOnly) ConfigureBucketSizes(bcs []internal.BucketConfiguration) { + for _, bc := range bcs { + latencyBucketSize := bc.MaxRequestsSucceeded + if latencyBucketSize < 0 { + latencyBucketSize = 0 + } + if latencyBucketSize > maxBucketSize { + latencyBucketSize = maxBucketSize + } + errorBucketSize := bc.MaxRequestsErrors + if errorBucketSize < 0 { + errorBucketSize = 0 + } + if errorBucketSize > maxBucketSize { + errorBucketSize = maxBucketSize + } + spanStoreSetSize(bc.Name, latencyBucketSize, errorBucketSize) + } +} + +// ReportSpansPerMethod returns a summary of what spans are being stored for each span name. +func (i internalOnly) ReportSpansPerMethod() map[string]internal.PerMethodSummary { + out := make(map[string]internal.PerMethodSummary) + ssmu.RLock() + defer ssmu.RUnlock() + for name, s := range spanStores { + s.mu.Lock() + p := internal.PerMethodSummary{ + Active: len(s.active), + } + for code, b := range s.errors { + p.ErrorBuckets = append(p.ErrorBuckets, internal.ErrorBucketSummary{ + ErrorCode: code, + Size: b.size(), + }) + } + for i, b := range s.latency { + min, max := latencyBucketBounds(i) + p.LatencyBuckets = append(p.LatencyBuckets, internal.LatencyBucketSummary{ + MinLatency: min, + MaxLatency: max, + Size: b.size(), + }) + } + s.mu.Unlock() + out[name] = p + } + return out +} + +// ReportSpansByLatency returns a sample of successful spans. +// +// minLatency is the minimum latency of spans to be returned. +// maxLatency, if nonzero, is the maximum latency of spans to be returned. +func (i internalOnly) ReportSpansByLatency(name string, minLatency, maxLatency time.Duration) []*SpanData { + s := spanStoreForName(name) + if s == nil { + return nil + } + var out []*SpanData + s.mu.Lock() + defer s.mu.Unlock() + for i, b := range s.latency { + min, max := latencyBucketBounds(i) + if i+1 != len(s.latency) && max <= minLatency { + continue + } + if maxLatency != 0 && maxLatency < min { + continue + } + for _, sd := range b.buffer { + if sd == nil { + break + } + if minLatency != 0 || maxLatency != 0 { + d := sd.EndTime.Sub(sd.StartTime) + if d < minLatency { + continue + } + if maxLatency != 0 && d > maxLatency { + continue + } + } + out = append(out, sd) + } + } + return out +} + +// spanStore keeps track of spans stored for a particular span name. +// +// It contains all active spans; a sample of spans for failed requests, +// categorized by error code; and a sample of spans for successful requests, +// bucketed by latency. +type spanStore struct { + mu sync.Mutex // protects everything below. + active map[*Span]struct{} + errors map[int32]*bucket + latency []bucket + maxSpansPerErrorBucket int +} + +// newSpanStore creates a span store. +func newSpanStore(name string, latencyBucketSize int, errorBucketSize int) *spanStore { + s := &spanStore{ + active: make(map[*Span]struct{}), + latency: make([]bucket, len(defaultLatencies)+1), + maxSpansPerErrorBucket: errorBucketSize, + } + for i := range s.latency { + s.latency[i] = makeBucket(latencyBucketSize) + } + return s +} + +// spanStoreForName returns the spanStore for the given name. +// +// It returns nil if it doesn't exist. +func spanStoreForName(name string) *spanStore { + var s *spanStore + ssmu.RLock() + s, _ = spanStores[name] + ssmu.RUnlock() + return s +} + +// spanStoreForNameCreateIfNew returns the spanStore for the given name. +// +// It creates it if it didn't exist. +func spanStoreForNameCreateIfNew(name string) *spanStore { + ssmu.RLock() + s, ok := spanStores[name] + ssmu.RUnlock() + if ok { + return s + } + ssmu.Lock() + defer ssmu.Unlock() + s, ok = spanStores[name] + if ok { + return s + } + s = newSpanStore(name, defaultBucketSize, defaultBucketSize) + spanStores[name] = s + return s +} + +// spanStoreSetSize resizes the spanStore for the given name. +// +// It creates it if it didn't exist. +func spanStoreSetSize(name string, latencyBucketSize int, errorBucketSize int) { + ssmu.RLock() + s, ok := spanStores[name] + ssmu.RUnlock() + if ok { + s.resize(latencyBucketSize, errorBucketSize) + return + } + ssmu.Lock() + defer ssmu.Unlock() + s, ok = spanStores[name] + if ok { + s.resize(latencyBucketSize, errorBucketSize) + return + } + s = newSpanStore(name, latencyBucketSize, errorBucketSize) + spanStores[name] = s +} + +func (s *spanStore) resize(latencyBucketSize int, errorBucketSize int) { + s.mu.Lock() + for i := range s.latency { + s.latency[i].resize(latencyBucketSize) + } + for _, b := range s.errors { + b.resize(errorBucketSize) + } + s.maxSpansPerErrorBucket = errorBucketSize + s.mu.Unlock() +} + +// add adds a span to the active bucket of the spanStore. +func (s *spanStore) add(span *Span) { + s.mu.Lock() + s.active[span] = struct{}{} + s.mu.Unlock() +} + +// finished removes a span from the active set, and adds a corresponding +// SpanData to a latency or error bucket. +func (s *spanStore) finished(span *Span, sd *SpanData) { + latency := sd.EndTime.Sub(sd.StartTime) + if latency < 0 { + latency = 0 + } + code := sd.Status.Code + + s.mu.Lock() + delete(s.active, span) + if code == 0 { + s.latency[latencyBucket(latency)].add(sd) + } else { + if s.errors == nil { + s.errors = make(map[int32]*bucket) + } + if b := s.errors[code]; b != nil { + b.add(sd) + } else { + b := makeBucket(s.maxSpansPerErrorBucket) + s.errors[code] = &b + b.add(sd) + } + } + s.mu.Unlock() +} diff --git a/vendor/go.opencensus.io/trace/status_codes.go b/vendor/go.opencensus.io/trace/status_codes.go new file mode 100644 index 0000000000..ec60effd10 --- /dev/null +++ b/vendor/go.opencensus.io/trace/status_codes.go @@ -0,0 +1,37 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +// Status codes for use with Span.SetStatus. These correspond to the status +// codes used by gRPC defined here: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto +const ( + StatusCodeOK = 0 + StatusCodeCancelled = 1 + StatusCodeUnknown = 2 + StatusCodeInvalidArgument = 3 + StatusCodeDeadlineExceeded = 4 + StatusCodeNotFound = 5 + StatusCodeAlreadyExists = 6 + StatusCodePermissionDenied = 7 + StatusCodeResourceExhausted = 8 + StatusCodeFailedPrecondition = 9 + StatusCodeAborted = 10 + StatusCodeOutOfRange = 11 + StatusCodeUnimplemented = 12 + StatusCodeInternal = 13 + StatusCodeUnavailable = 14 + StatusCodeDataLoss = 15 + StatusCodeUnauthenticated = 16 +) diff --git a/vendor/go.opencensus.io/trace/trace.go b/vendor/go.opencensus.io/trace/trace.go new file mode 100644 index 0000000000..9e5e5f0331 --- /dev/null +++ b/vendor/go.opencensus.io/trace/trace.go @@ -0,0 +1,516 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "context" + crand "crypto/rand" + "encoding/binary" + "fmt" + "math/rand" + "sync" + "sync/atomic" + "time" + + "go.opencensus.io/internal" + "go.opencensus.io/trace/tracestate" +) + +// Span represents a span of a trace. It has an associated SpanContext, and +// stores data accumulated while the span is active. +// +// Ideally users should interact with Spans by calling the functions in this +// package that take a Context parameter. +type Span struct { + // data contains information recorded about the span. + // + // It will be non-nil if we are exporting the span or recording events for it. + // Otherwise, data is nil, and the Span is simply a carrier for the + // SpanContext, so that the trace ID is propagated. + data *SpanData + mu sync.Mutex // protects the contents of *data (but not the pointer value.) + spanContext SpanContext + // spanStore is the spanStore this span belongs to, if any, otherwise it is nil. + *spanStore + endOnce sync.Once + + executionTracerTaskEnd func() // ends the execution tracer span +} + +// IsRecordingEvents returns true if events are being recorded for this span. +// Use this check to avoid computing expensive annotations when they will never +// be used. +func (s *Span) IsRecordingEvents() bool { + if s == nil { + return false + } + return s.data != nil +} + +// TraceOptions contains options associated with a trace span. +type TraceOptions uint32 + +// IsSampled returns true if the span will be exported. +func (sc SpanContext) IsSampled() bool { + return sc.TraceOptions.IsSampled() +} + +// setIsSampled sets the TraceOptions bit that determines whether the span will be exported. +func (sc *SpanContext) setIsSampled(sampled bool) { + if sampled { + sc.TraceOptions |= 1 + } else { + sc.TraceOptions &= ^TraceOptions(1) + } +} + +// IsSampled returns true if the span will be exported. +func (t TraceOptions) IsSampled() bool { + return t&1 == 1 +} + +// SpanContext contains the state that must propagate across process boundaries. +// +// SpanContext is not an implementation of context.Context. +// TODO: add reference to external Census docs for SpanContext. +type SpanContext struct { + TraceID TraceID + SpanID SpanID + TraceOptions TraceOptions + Tracestate *tracestate.Tracestate +} + +type contextKey struct{} + +// FromContext returns the Span stored in a context, or nil if there isn't one. +func FromContext(ctx context.Context) *Span { + s, _ := ctx.Value(contextKey{}).(*Span) + return s +} + +// NewContext returns a new context with the given Span attached. +func NewContext(parent context.Context, s *Span) context.Context { + return context.WithValue(parent, contextKey{}, s) +} + +// All available span kinds. Span kind must be either one of these values. +const ( + SpanKindUnspecified = iota + SpanKindServer + SpanKindClient +) + +// StartOptions contains options concerning how a span is started. +type StartOptions struct { + // Sampler to consult for this Span. If provided, it is always consulted. + // + // If not provided, then the behavior differs based on whether + // the parent of this Span is remote, local, or there is no parent. + // In the case of a remote parent or no parent, the + // default sampler (see Config) will be consulted. Otherwise, + // when there is a non-remote parent, no new sampling decision will be made: + // we will preserve the sampling of the parent. + Sampler Sampler + + // SpanKind represents the kind of a span. If none is set, + // SpanKindUnspecified is used. + SpanKind int +} + +// StartOption apply changes to StartOptions. +type StartOption func(*StartOptions) + +// WithSpanKind makes new spans to be created with the given kind. +func WithSpanKind(spanKind int) StartOption { + return func(o *StartOptions) { + o.SpanKind = spanKind + } +} + +// WithSampler makes new spans to be be created with a custom sampler. +// Otherwise, the global sampler is used. +func WithSampler(sampler Sampler) StartOption { + return func(o *StartOptions) { + o.Sampler = sampler + } +} + +// StartSpan starts a new child span of the current span in the context. If +// there is no span in the context, creates a new trace and span. +// +// Returned context contains the newly created span. You can use it to +// propagate the returned span in process. +func StartSpan(ctx context.Context, name string, o ...StartOption) (context.Context, *Span) { + var opts StartOptions + var parent SpanContext + if p := FromContext(ctx); p != nil { + parent = p.spanContext + } + for _, op := range o { + op(&opts) + } + span := startSpanInternal(name, parent != SpanContext{}, parent, false, opts) + + ctx, end := startExecutionTracerTask(ctx, name) + span.executionTracerTaskEnd = end + return NewContext(ctx, span), span +} + +// StartSpanWithRemoteParent starts a new child span of the span from the given parent. +// +// If the incoming context contains a parent, it ignores. StartSpanWithRemoteParent is +// preferred for cases where the parent is propagated via an incoming request. +// +// Returned context contains the newly created span. You can use it to +// propagate the returned span in process. +func StartSpanWithRemoteParent(ctx context.Context, name string, parent SpanContext, o ...StartOption) (context.Context, *Span) { + var opts StartOptions + for _, op := range o { + op(&opts) + } + span := startSpanInternal(name, parent != SpanContext{}, parent, true, opts) + ctx, end := startExecutionTracerTask(ctx, name) + span.executionTracerTaskEnd = end + return NewContext(ctx, span), span +} + +func startSpanInternal(name string, hasParent bool, parent SpanContext, remoteParent bool, o StartOptions) *Span { + span := &Span{} + span.spanContext = parent + + cfg := config.Load().(*Config) + + if !hasParent { + span.spanContext.TraceID = cfg.IDGenerator.NewTraceID() + } + span.spanContext.SpanID = cfg.IDGenerator.NewSpanID() + sampler := cfg.DefaultSampler + + if !hasParent || remoteParent || o.Sampler != nil { + // If this span is the child of a local span and no Sampler is set in the + // options, keep the parent's TraceOptions. + // + // Otherwise, consult the Sampler in the options if it is non-nil, otherwise + // the default sampler. + if o.Sampler != nil { + sampler = o.Sampler + } + span.spanContext.setIsSampled(sampler(SamplingParameters{ + ParentContext: parent, + TraceID: span.spanContext.TraceID, + SpanID: span.spanContext.SpanID, + Name: name, + HasRemoteParent: remoteParent}).Sample) + } + + if !internal.LocalSpanStoreEnabled && !span.spanContext.IsSampled() { + return span + } + + span.data = &SpanData{ + SpanContext: span.spanContext, + StartTime: time.Now(), + SpanKind: o.SpanKind, + Name: name, + HasRemoteParent: remoteParent, + } + if hasParent { + span.data.ParentSpanID = parent.SpanID + } + if internal.LocalSpanStoreEnabled { + var ss *spanStore + ss = spanStoreForNameCreateIfNew(name) + if ss != nil { + span.spanStore = ss + ss.add(span) + } + } + + return span +} + +// End ends the span. +func (s *Span) End() { + if s == nil { + return + } + if s.executionTracerTaskEnd != nil { + s.executionTracerTaskEnd() + } + if !s.IsRecordingEvents() { + return + } + s.endOnce.Do(func() { + exp, _ := exporters.Load().(exportersMap) + mustExport := s.spanContext.IsSampled() && len(exp) > 0 + if s.spanStore != nil || mustExport { + sd := s.makeSpanData() + sd.EndTime = internal.MonotonicEndTime(sd.StartTime) + if s.spanStore != nil { + s.spanStore.finished(s, sd) + } + if mustExport { + for e := range exp { + e.ExportSpan(sd) + } + } + } + }) +} + +// makeSpanData produces a SpanData representing the current state of the Span. +// It requires that s.data is non-nil. +func (s *Span) makeSpanData() *SpanData { + var sd SpanData + s.mu.Lock() + sd = *s.data + if s.data.Attributes != nil { + sd.Attributes = make(map[string]interface{}) + for k, v := range s.data.Attributes { + sd.Attributes[k] = v + } + } + s.mu.Unlock() + return &sd +} + +// SpanContext returns the SpanContext of the span. +func (s *Span) SpanContext() SpanContext { + if s == nil { + return SpanContext{} + } + return s.spanContext +} + +// SetName sets the name of the span, if it is recording events. +func (s *Span) SetName(name string) { + if !s.IsRecordingEvents() { + return + } + s.mu.Lock() + s.data.Name = name + s.mu.Unlock() +} + +// SetStatus sets the status of the span, if it is recording events. +func (s *Span) SetStatus(status Status) { + if !s.IsRecordingEvents() { + return + } + s.mu.Lock() + s.data.Status = status + s.mu.Unlock() +} + +// AddAttributes sets attributes in the span. +// +// Existing attributes whose keys appear in the attributes parameter are overwritten. +func (s *Span) AddAttributes(attributes ...Attribute) { + if !s.IsRecordingEvents() { + return + } + s.mu.Lock() + if s.data.Attributes == nil { + s.data.Attributes = make(map[string]interface{}) + } + copyAttributes(s.data.Attributes, attributes) + s.mu.Unlock() +} + +// copyAttributes copies a slice of Attributes into a map. +func copyAttributes(m map[string]interface{}, attributes []Attribute) { + for _, a := range attributes { + m[a.key] = a.value + } +} + +func (s *Span) lazyPrintfInternal(attributes []Attribute, format string, a ...interface{}) { + now := time.Now() + msg := fmt.Sprintf(format, a...) + var m map[string]interface{} + s.mu.Lock() + if len(attributes) != 0 { + m = make(map[string]interface{}) + copyAttributes(m, attributes) + } + s.data.Annotations = append(s.data.Annotations, Annotation{ + Time: now, + Message: msg, + Attributes: m, + }) + s.mu.Unlock() +} + +func (s *Span) printStringInternal(attributes []Attribute, str string) { + now := time.Now() + var a map[string]interface{} + s.mu.Lock() + if len(attributes) != 0 { + a = make(map[string]interface{}) + copyAttributes(a, attributes) + } + s.data.Annotations = append(s.data.Annotations, Annotation{ + Time: now, + Message: str, + Attributes: a, + }) + s.mu.Unlock() +} + +// Annotate adds an annotation with attributes. +// Attributes can be nil. +func (s *Span) Annotate(attributes []Attribute, str string) { + if !s.IsRecordingEvents() { + return + } + s.printStringInternal(attributes, str) +} + +// Annotatef adds an annotation with attributes. +func (s *Span) Annotatef(attributes []Attribute, format string, a ...interface{}) { + if !s.IsRecordingEvents() { + return + } + s.lazyPrintfInternal(attributes, format, a...) +} + +// AddMessageSendEvent adds a message send event to the span. +// +// messageID is an identifier for the message, which is recommended to be +// unique in this span and the same between the send event and the receive +// event (this allows to identify a message between the sender and receiver). +// For example, this could be a sequence id. +func (s *Span) AddMessageSendEvent(messageID, uncompressedByteSize, compressedByteSize int64) { + if !s.IsRecordingEvents() { + return + } + now := time.Now() + s.mu.Lock() + s.data.MessageEvents = append(s.data.MessageEvents, MessageEvent{ + Time: now, + EventType: MessageEventTypeSent, + MessageID: messageID, + UncompressedByteSize: uncompressedByteSize, + CompressedByteSize: compressedByteSize, + }) + s.mu.Unlock() +} + +// AddMessageReceiveEvent adds a message receive event to the span. +// +// messageID is an identifier for the message, which is recommended to be +// unique in this span and the same between the send event and the receive +// event (this allows to identify a message between the sender and receiver). +// For example, this could be a sequence id. +func (s *Span) AddMessageReceiveEvent(messageID, uncompressedByteSize, compressedByteSize int64) { + if !s.IsRecordingEvents() { + return + } + now := time.Now() + s.mu.Lock() + s.data.MessageEvents = append(s.data.MessageEvents, MessageEvent{ + Time: now, + EventType: MessageEventTypeRecv, + MessageID: messageID, + UncompressedByteSize: uncompressedByteSize, + CompressedByteSize: compressedByteSize, + }) + s.mu.Unlock() +} + +// AddLink adds a link to the span. +func (s *Span) AddLink(l Link) { + if !s.IsRecordingEvents() { + return + } + s.mu.Lock() + s.data.Links = append(s.data.Links, l) + s.mu.Unlock() +} + +func (s *Span) String() string { + if s == nil { + return "" + } + if s.data == nil { + return fmt.Sprintf("span %s", s.spanContext.SpanID) + } + s.mu.Lock() + str := fmt.Sprintf("span %s %q", s.spanContext.SpanID, s.data.Name) + s.mu.Unlock() + return str +} + +var config atomic.Value // access atomically + +func init() { + gen := &defaultIDGenerator{} + // initialize traceID and spanID generators. + var rngSeed int64 + for _, p := range []interface{}{ + &rngSeed, &gen.traceIDAdd, &gen.nextSpanID, &gen.spanIDInc, + } { + binary.Read(crand.Reader, binary.LittleEndian, p) + } + gen.traceIDRand = rand.New(rand.NewSource(rngSeed)) + gen.spanIDInc |= 1 + + config.Store(&Config{ + DefaultSampler: ProbabilitySampler(defaultSamplingProbability), + IDGenerator: gen, + }) +} + +type defaultIDGenerator struct { + sync.Mutex + + // Please keep these as the first fields + // so that these 8 byte fields will be aligned on addresses + // divisible by 8, on both 32-bit and 64-bit machines when + // performing atomic increments and accesses. + // See: + // * https://github.com/census-instrumentation/opencensus-go/issues/587 + // * https://github.com/census-instrumentation/opencensus-go/issues/865 + // * https://golang.org/pkg/sync/atomic/#pkg-note-BUG + nextSpanID uint64 + spanIDInc uint64 + + traceIDAdd [2]uint64 + traceIDRand *rand.Rand +} + +// NewSpanID returns a non-zero span ID from a randomly-chosen sequence. +func (gen *defaultIDGenerator) NewSpanID() [8]byte { + var id uint64 + for id == 0 { + id = atomic.AddUint64(&gen.nextSpanID, gen.spanIDInc) + } + var sid [8]byte + binary.LittleEndian.PutUint64(sid[:], id) + return sid +} + +// NewTraceID returns a non-zero trace ID from a randomly-chosen sequence. +// mu should be held while this function is called. +func (gen *defaultIDGenerator) NewTraceID() [16]byte { + var tid [16]byte + // Construct the trace ID from two outputs of traceIDRand, with a constant + // added to each half for additional entropy. + gen.Lock() + binary.LittleEndian.PutUint64(tid[0:8], gen.traceIDRand.Uint64()+gen.traceIDAdd[0]) + binary.LittleEndian.PutUint64(tid[8:16], gen.traceIDRand.Uint64()+gen.traceIDAdd[1]) + gen.Unlock() + return tid +} diff --git a/vendor/go.opencensus.io/trace/trace_go11.go b/vendor/go.opencensus.io/trace/trace_go11.go new file mode 100644 index 0000000000..b7d8aaf284 --- /dev/null +++ b/vendor/go.opencensus.io/trace/trace_go11.go @@ -0,0 +1,32 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build go1.11 + +package trace + +import ( + "context" + t "runtime/trace" +) + +func startExecutionTracerTask(ctx context.Context, name string) (context.Context, func()) { + if !t.IsEnabled() { + // Avoid additional overhead if + // runtime/trace is not enabled. + return ctx, func() {} + } + nctx, task := t.NewTask(ctx, name) + return nctx, task.End +} diff --git a/vendor/go.opencensus.io/trace/trace_nongo11.go b/vendor/go.opencensus.io/trace/trace_nongo11.go new file mode 100644 index 0000000000..e25419859c --- /dev/null +++ b/vendor/go.opencensus.io/trace/trace_nongo11.go @@ -0,0 +1,25 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !go1.11 + +package trace + +import ( + "context" +) + +func startExecutionTracerTask(ctx context.Context, name string) (context.Context, func()) { + return ctx, func() {} +} diff --git a/vendor/go.opencensus.io/trace/trace_test.go b/vendor/go.opencensus.io/trace/trace_test.go new file mode 100644 index 0000000000..0c25cc7f36 --- /dev/null +++ b/vendor/go.opencensus.io/trace/trace_test.go @@ -0,0 +1,714 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "context" + "fmt" + "reflect" + "sync/atomic" + "testing" + "time" + + "go.opencensus.io/trace/tracestate" +) + +var ( + tid = TraceID{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 4, 8, 16, 32, 64, 128} + sid = SpanID{1, 2, 4, 8, 16, 32, 64, 128} + testTracestate, _ = tracestate.New(nil, tracestate.Entry{Key: "foo", Value: "bar"}) +) + +func init() { + // no random sampling, but sample children of sampled spans. + ApplyConfig(Config{DefaultSampler: ProbabilitySampler(0)}) +} + +func TestStrings(t *testing.T) { + if got, want := tid.String(), "01020304050607080102040810204080"; got != want { + t.Errorf("TraceID.String: got %q want %q", got, want) + } + if got, want := sid.String(), "0102040810204080"; got != want { + t.Errorf("SpanID.String: got %q want %q", got, want) + } +} + +func TestFromContext(t *testing.T) { + want := &Span{} + ctx := NewContext(context.Background(), want) + got := FromContext(ctx) + if got != want { + t.Errorf("got Span pointer %p want %p", got, want) + } +} + +type foo int + +func (f foo) String() string { + return "foo" +} + +// checkChild tests that c has fields set appropriately, given that it is a child span of p. +func checkChild(p SpanContext, c *Span) error { + if c == nil { + return fmt.Errorf("got nil child span, want non-nil") + } + if got, want := c.spanContext.TraceID, p.TraceID; got != want { + return fmt.Errorf("got child trace ID %s, want %s", got, want) + } + if childID, parentID := c.spanContext.SpanID, p.SpanID; childID == parentID { + return fmt.Errorf("got child span ID %s, parent span ID %s; want unequal IDs", childID, parentID) + } + if got, want := c.spanContext.TraceOptions, p.TraceOptions; got != want { + return fmt.Errorf("got child trace options %d, want %d", got, want) + } + if got, want := c.spanContext.Tracestate, p.Tracestate; got != want { + return fmt.Errorf("got child tracestate %v, want %v", got, want) + } + return nil +} + +func TestStartSpan(t *testing.T) { + ctx, _ := StartSpan(context.Background(), "StartSpan") + if FromContext(ctx).data != nil { + t.Error("StartSpan: new span is recording events") + } +} + +func TestSampling(t *testing.T) { + for _, test := range []struct { + remoteParent bool + localParent bool + parentTraceOptions TraceOptions + sampler Sampler + wantTraceOptions TraceOptions + }{ + {true, false, 0, nil, 0}, + {true, false, 1, nil, 1}, + {true, false, 0, NeverSample(), 0}, + {true, false, 1, NeverSample(), 0}, + {true, false, 0, AlwaysSample(), 1}, + {true, false, 1, AlwaysSample(), 1}, + {false, true, 0, NeverSample(), 0}, + {false, true, 1, NeverSample(), 0}, + {false, true, 0, AlwaysSample(), 1}, + {false, true, 1, AlwaysSample(), 1}, + {false, false, 0, nil, 0}, + {false, false, 0, NeverSample(), 0}, + {false, false, 0, AlwaysSample(), 1}, + } { + var ctx context.Context + if test.remoteParent { + sc := SpanContext{ + TraceID: tid, + SpanID: sid, + TraceOptions: test.parentTraceOptions, + } + ctx, _ = StartSpanWithRemoteParent(context.Background(), "foo", sc, WithSampler(test.sampler)) + } else if test.localParent { + sampler := NeverSample() + if test.parentTraceOptions == 1 { + sampler = AlwaysSample() + } + ctx2, _ := StartSpan(context.Background(), "foo", WithSampler(sampler)) + ctx, _ = StartSpan(ctx2, "foo", WithSampler(test.sampler)) + } else { + ctx, _ = StartSpan(context.Background(), "foo", WithSampler(test.sampler)) + } + sc := FromContext(ctx).SpanContext() + if (sc == SpanContext{}) { + t.Errorf("case %#v: starting new span: no span in context", test) + continue + } + if sc.SpanID == (SpanID{}) { + t.Errorf("case %#v: starting new span: got zero SpanID, want nonzero", test) + } + if sc.TraceOptions != test.wantTraceOptions { + t.Errorf("case %#v: starting new span: got TraceOptions %x, want %x", test, sc.TraceOptions, test.wantTraceOptions) + } + } + + // Test that for children of local spans, the default sampler has no effect. + for _, test := range []struct { + parentTraceOptions TraceOptions + wantTraceOptions TraceOptions + }{ + {0, 0}, + {0, 0}, + {1, 1}, + {1, 1}, + } { + for _, defaultSampler := range []Sampler{ + NeverSample(), + AlwaysSample(), + ProbabilitySampler(0), + } { + ApplyConfig(Config{DefaultSampler: defaultSampler}) + sampler := NeverSample() + if test.parentTraceOptions == 1 { + sampler = AlwaysSample() + } + ctx2, _ := StartSpan(context.Background(), "foo", WithSampler(sampler)) + ctx, _ := StartSpan(ctx2, "foo") + sc := FromContext(ctx).SpanContext() + if (sc == SpanContext{}) { + t.Errorf("case %#v: starting new child of local span: no span in context", test) + continue + } + if sc.SpanID == (SpanID{}) { + t.Errorf("case %#v: starting new child of local span: got zero SpanID, want nonzero", test) + } + if sc.TraceOptions != test.wantTraceOptions { + t.Errorf("case %#v: starting new child of local span: got TraceOptions %x, want %x", test, sc.TraceOptions, test.wantTraceOptions) + } + } + } + ApplyConfig(Config{DefaultSampler: ProbabilitySampler(0)}) // reset the default sampler. +} + +func TestProbabilitySampler(t *testing.T) { + exported := 0 + for i := 0; i < 1000; i++ { + _, span := StartSpan(context.Background(), "foo", WithSampler(ProbabilitySampler(0.3))) + if span.SpanContext().IsSampled() { + exported++ + } + } + if exported < 200 || exported > 400 { + t.Errorf("got %f%% exported spans, want approximately 30%%", float64(exported)*0.1) + } +} + +func TestStartSpanWithRemoteParent(t *testing.T) { + sc := SpanContext{ + TraceID: tid, + SpanID: sid, + TraceOptions: 0x0, + } + ctx, _ := StartSpanWithRemoteParent(context.Background(), "startSpanWithRemoteParent", sc) + if err := checkChild(sc, FromContext(ctx)); err != nil { + t.Error(err) + } + + ctx, _ = StartSpanWithRemoteParent(context.Background(), "startSpanWithRemoteParent", sc) + if err := checkChild(sc, FromContext(ctx)); err != nil { + t.Error(err) + } + + sc = SpanContext{ + TraceID: tid, + SpanID: sid, + TraceOptions: 0x1, + Tracestate: testTracestate, + } + ctx, _ = StartSpanWithRemoteParent(context.Background(), "startSpanWithRemoteParent", sc) + if err := checkChild(sc, FromContext(ctx)); err != nil { + t.Error(err) + } + + ctx, _ = StartSpanWithRemoteParent(context.Background(), "startSpanWithRemoteParent", sc) + if err := checkChild(sc, FromContext(ctx)); err != nil { + t.Error(err) + } + + ctx2, _ := StartSpan(ctx, "StartSpan") + parent := FromContext(ctx).SpanContext() + if err := checkChild(parent, FromContext(ctx2)); err != nil { + t.Error(err) + } +} + +// startSpan returns a context with a new Span that is recording events and will be exported. +func startSpan(o StartOptions) *Span { + _, span := StartSpanWithRemoteParent(context.Background(), "span0", + SpanContext{ + TraceID: tid, + SpanID: sid, + TraceOptions: 1, + }, + WithSampler(o.Sampler), + WithSpanKind(o.SpanKind), + ) + return span +} + +type testExporter struct { + spans []*SpanData +} + +func (t *testExporter) ExportSpan(s *SpanData) { + t.spans = append(t.spans, s) +} + +// endSpan ends the Span in the context and returns the exported SpanData. +// +// It also does some tests on the Span, and tests and clears some fields in the SpanData. +func endSpan(span *Span) (*SpanData, error) { + + if !span.IsRecordingEvents() { + return nil, fmt.Errorf("IsRecordingEvents: got false, want true") + } + if !span.SpanContext().IsSampled() { + return nil, fmt.Errorf("IsSampled: got false, want true") + } + var te testExporter + RegisterExporter(&te) + span.End() + UnregisterExporter(&te) + if len(te.spans) != 1 { + return nil, fmt.Errorf("got exported spans %#v, want one span", te.spans) + } + got := te.spans[0] + if got.SpanContext.SpanID == (SpanID{}) { + return nil, fmt.Errorf("exporting span: expected nonzero SpanID") + } + got.SpanContext.SpanID = SpanID{} + if !checkTime(&got.StartTime) { + return nil, fmt.Errorf("exporting span: expected nonzero StartTime") + } + if !checkTime(&got.EndTime) { + return nil, fmt.Errorf("exporting span: expected nonzero EndTime") + } + return got, nil +} + +// checkTime checks that a nonzero time was set in x, then clears it. +func checkTime(x *time.Time) bool { + if x.IsZero() { + return false + } + *x = time.Time{} + return true +} + +func TestSpanKind(t *testing.T) { + tests := []struct { + name string + startOptions StartOptions + want *SpanData + }{ + { + name: "zero StartOptions", + startOptions: StartOptions{}, + want: &SpanData{ + SpanContext: SpanContext{ + TraceID: tid, + SpanID: SpanID{}, + TraceOptions: 0x1, + }, + ParentSpanID: sid, + Name: "span0", + SpanKind: SpanKindUnspecified, + HasRemoteParent: true, + }, + }, + { + name: "client span", + startOptions: StartOptions{ + SpanKind: SpanKindClient, + }, + want: &SpanData{ + SpanContext: SpanContext{ + TraceID: tid, + SpanID: SpanID{}, + TraceOptions: 0x1, + }, + ParentSpanID: sid, + Name: "span0", + SpanKind: SpanKindClient, + HasRemoteParent: true, + }, + }, + { + name: "server span", + startOptions: StartOptions{ + SpanKind: SpanKindServer, + }, + want: &SpanData{ + SpanContext: SpanContext{ + TraceID: tid, + SpanID: SpanID{}, + TraceOptions: 0x1, + }, + ParentSpanID: sid, + Name: "span0", + SpanKind: SpanKindServer, + HasRemoteParent: true, + }, + }, + } + + for _, tt := range tests { + span := startSpan(tt.startOptions) + got, err := endSpan(span) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("exporting span: got %#v want %#v", got, tt.want) + } + } +} + +func TestSetSpanAttributes(t *testing.T) { + span := startSpan(StartOptions{}) + span.AddAttributes(StringAttribute("key1", "value1")) + got, err := endSpan(span) + if err != nil { + t.Fatal(err) + } + + want := &SpanData{ + SpanContext: SpanContext{ + TraceID: tid, + SpanID: SpanID{}, + TraceOptions: 0x1, + }, + ParentSpanID: sid, + Name: "span0", + Attributes: map[string]interface{}{"key1": "value1"}, + HasRemoteParent: true, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("exporting span: got %#v want %#v", got, want) + } +} + +func TestAnnotations(t *testing.T) { + span := startSpan(StartOptions{}) + span.Annotatef([]Attribute{StringAttribute("key1", "value1")}, "%f", 1.5) + span.Annotate([]Attribute{StringAttribute("key2", "value2")}, "Annotate") + got, err := endSpan(span) + if err != nil { + t.Fatal(err) + } + + for i := range got.Annotations { + if !checkTime(&got.Annotations[i].Time) { + t.Error("exporting span: expected nonzero Annotation Time") + } + } + + want := &SpanData{ + SpanContext: SpanContext{ + TraceID: tid, + SpanID: SpanID{}, + TraceOptions: 0x1, + }, + ParentSpanID: sid, + Name: "span0", + Annotations: []Annotation{ + {Message: "1.500000", Attributes: map[string]interface{}{"key1": "value1"}}, + {Message: "Annotate", Attributes: map[string]interface{}{"key2": "value2"}}, + }, + HasRemoteParent: true, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("exporting span: got %#v want %#v", got, want) + } +} + +func TestMessageEvents(t *testing.T) { + span := startSpan(StartOptions{}) + span.AddMessageReceiveEvent(3, 400, 300) + span.AddMessageSendEvent(1, 200, 100) + got, err := endSpan(span) + if err != nil { + t.Fatal(err) + } + + for i := range got.MessageEvents { + if !checkTime(&got.MessageEvents[i].Time) { + t.Error("exporting span: expected nonzero MessageEvent Time") + } + } + + want := &SpanData{ + SpanContext: SpanContext{ + TraceID: tid, + SpanID: SpanID{}, + TraceOptions: 0x1, + }, + ParentSpanID: sid, + Name: "span0", + MessageEvents: []MessageEvent{ + {EventType: 2, MessageID: 0x3, UncompressedByteSize: 0x190, CompressedByteSize: 0x12c}, + {EventType: 1, MessageID: 0x1, UncompressedByteSize: 0xc8, CompressedByteSize: 0x64}, + }, + HasRemoteParent: true, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("exporting span: got %#v want %#v", got, want) + } +} + +func TestSetSpanName(t *testing.T) { + want := "SpanName-1" + span := startSpan(StartOptions{}) + span.SetName(want) + got, err := endSpan(span) + if err != nil { + t.Fatal(err) + } + + if got.Name != want { + t.Errorf("span.Name=%q; want %q", got.Name, want) + } +} + +func TestSetSpanNameUnsampledSpan(t *testing.T) { + var nilSpanData *SpanData + span := startSpan(StartOptions{Sampler: NeverSample()}) + span.SetName("NoopName") + + if want, got := nilSpanData, span.data; want != got { + t.Errorf("span.data=%+v; want %+v", got, want) + } +} + +func TestSetSpanNameAfterSpanEnd(t *testing.T) { + want := "SpanName-2" + span := startSpan(StartOptions{}) + span.SetName(want) + got, err := endSpan(span) + if err != nil { + t.Fatal(err) + } + + // updating name after span.End + span.SetName("NoopName") + + // exported span should not be updated by previous call to SetName + if got.Name != want { + t.Errorf("span.Name=%q; want %q", got.Name, want) + } + + // span should not be exported again + var te testExporter + RegisterExporter(&te) + span.End() + UnregisterExporter(&te) + if len(te.spans) != 0 { + t.Errorf("got exported spans %#v, wanted no spans", te.spans) + } +} + +func TestSetSpanStatus(t *testing.T) { + span := startSpan(StartOptions{}) + span.SetStatus(Status{Code: int32(1), Message: "request failed"}) + got, err := endSpan(span) + if err != nil { + t.Fatal(err) + } + + want := &SpanData{ + SpanContext: SpanContext{ + TraceID: tid, + SpanID: SpanID{}, + TraceOptions: 0x1, + }, + ParentSpanID: sid, + Name: "span0", + Status: Status{Code: 1, Message: "request failed"}, + HasRemoteParent: true, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("exporting span: got %#v want %#v", got, want) + } +} + +func TestAddLink(t *testing.T) { + span := startSpan(StartOptions{}) + span.AddLink(Link{ + TraceID: tid, + SpanID: sid, + Type: LinkTypeParent, + Attributes: map[string]interface{}{"key5": "value5"}, + }) + got, err := endSpan(span) + if err != nil { + t.Fatal(err) + } + + want := &SpanData{ + SpanContext: SpanContext{ + TraceID: tid, + SpanID: SpanID{}, + TraceOptions: 0x1, + }, + ParentSpanID: sid, + Name: "span0", + Links: []Link{{ + TraceID: tid, + SpanID: sid, + Type: 2, + Attributes: map[string]interface{}{"key5": "value5"}, + }}, + HasRemoteParent: true, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("exporting span: got %#v want %#v", got, want) + } +} + +func TestUnregisterExporter(t *testing.T) { + var te testExporter + RegisterExporter(&te) + UnregisterExporter(&te) + + ctx := startSpan(StartOptions{}) + endSpan(ctx) + if len(te.spans) != 0 { + t.Error("unregistered Exporter was called") + } +} + +func TestBucket(t *testing.T) { + // make a bucket of size 5 and add 10 spans + b := makeBucket(5) + for i := 1; i <= 10; i++ { + b.nextTime = time.Time{} // reset the time so that the next span is accepted. + // add a span, with i stored in the TraceID so we can test for it later. + b.add(&SpanData{SpanContext: SpanContext{TraceID: TraceID{byte(i)}}, EndTime: time.Now()}) + if i <= 5 { + if b.size() != i { + t.Fatalf("got bucket size %d, want %d %#v\n", b.size(), i, b) + } + for j := 0; j < i; j++ { + if b.span(j).TraceID[0] != byte(j+1) { + t.Errorf("got span index %d, want %d\n", b.span(j).TraceID[0], j+1) + } + } + } else { + if b.size() != 5 { + t.Fatalf("got bucket size %d, want 5\n", b.size()) + } + for j := 0; j < 5; j++ { + want := i - 4 + j + if b.span(j).TraceID[0] != byte(want) { + t.Errorf("got span index %d, want %d\n", b.span(j).TraceID[0], want) + } + } + } + } + // expand the bucket + b.resize(20) + if b.size() != 5 { + t.Fatalf("after resizing upwards: got bucket size %d, want 5\n", b.size()) + } + for i := 0; i < 5; i++ { + want := 6 + i + if b.span(i).TraceID[0] != byte(want) { + t.Errorf("after resizing upwards: got span index %d, want %d\n", b.span(i).TraceID[0], want) + } + } + // shrink the bucket + b.resize(3) + if b.size() != 3 { + t.Fatalf("after resizing downwards: got bucket size %d, want 3\n", b.size()) + } + for i := 0; i < 3; i++ { + want := 8 + i + if b.span(i).TraceID[0] != byte(want) { + t.Errorf("after resizing downwards: got span index %d, want %d\n", b.span(i).TraceID[0], want) + } + } +} + +type exporter map[string]*SpanData + +func (e exporter) ExportSpan(s *SpanData) { + e[s.Name] = s +} + +func Test_Issue328_EndSpanTwice(t *testing.T) { + spans := make(exporter) + RegisterExporter(&spans) + defer UnregisterExporter(&spans) + ctx := context.Background() + ctx, span := StartSpan(ctx, "span-1", WithSampler(AlwaysSample())) + span.End() + span.End() + UnregisterExporter(&spans) + if len(spans) != 1 { + t.Fatalf("expected only a single span, got %#v", spans) + } +} + +func TestStartSpanAfterEnd(t *testing.T) { + spans := make(exporter) + RegisterExporter(&spans) + defer UnregisterExporter(&spans) + ctx, span0 := StartSpan(context.Background(), "parent", WithSampler(AlwaysSample())) + ctx1, span1 := StartSpan(ctx, "span-1", WithSampler(AlwaysSample())) + span1.End() + // Start a new span with the context containing span-1 + // even though span-1 is ended, we still add this as a new child of span-1 + _, span2 := StartSpan(ctx1, "span-2", WithSampler(AlwaysSample())) + span2.End() + span0.End() + UnregisterExporter(&spans) + if got, want := len(spans), 3; got != want { + t.Fatalf("len(%#v) = %d; want %d", spans, got, want) + } + if got, want := spans["span-1"].TraceID, spans["parent"].TraceID; got != want { + t.Errorf("span-1.TraceID=%q; want %q", got, want) + } + if got, want := spans["span-2"].TraceID, spans["parent"].TraceID; got != want { + t.Errorf("span-2.TraceID=%q; want %q", got, want) + } + if got, want := spans["span-1"].ParentSpanID, spans["parent"].SpanID; got != want { + t.Errorf("span-1.ParentSpanID=%q; want %q (parent.SpanID)", got, want) + } + if got, want := spans["span-2"].ParentSpanID, spans["span-1"].SpanID; got != want { + t.Errorf("span-2.ParentSpanID=%q; want %q (span1.SpanID)", got, want) + } +} + +func TestNilSpanEnd(t *testing.T) { + var span *Span + span.End() +} + +func TestExecutionTracerTaskEnd(t *testing.T) { + var n uint64 + executionTracerTaskEnd := func() { + atomic.AddUint64(&n, 1) + } + + var spans []*Span + _, span := StartSpan(context.Background(), "foo", WithSampler(NeverSample())) + span.executionTracerTaskEnd = executionTracerTaskEnd + spans = append(spans, span) // never sample + + _, span = StartSpanWithRemoteParent(context.Background(), "foo", SpanContext{ + TraceID: TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + SpanID: SpanID{0, 1, 2, 3, 4, 5, 6, 7}, + TraceOptions: 0, + }) + span.executionTracerTaskEnd = executionTracerTaskEnd + spans = append(spans, span) // parent not sampled + + _, span = StartSpan(context.Background(), "foo", WithSampler(AlwaysSample())) + span.executionTracerTaskEnd = executionTracerTaskEnd + spans = append(spans, span) // always sample + + for _, span := range spans { + span.End() + } + if got, want := n, uint64(len(spans)); got != want { + t.Fatalf("Execution tracer task ended for %v spans; want %v", got, want) + } +} diff --git a/vendor/go.opencensus.io/trace/tracestate/tracestate.go b/vendor/go.opencensus.io/trace/tracestate/tracestate.go new file mode 100644 index 0000000000..2d6c713eb3 --- /dev/null +++ b/vendor/go.opencensus.io/trace/tracestate/tracestate.go @@ -0,0 +1,147 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tracestate implements support for the Tracestate header of the +// W3C TraceContext propagation format. +package tracestate + +import ( + "fmt" + "regexp" +) + +const ( + keyMaxSize = 256 + valueMaxSize = 256 + maxKeyValuePairs = 32 +) + +const ( + keyWithoutVendorFormat = `[a-z][_0-9a-z\-\*\/]{0,255}` + keyWithVendorFormat = `[a-z][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}` + keyFormat = `(` + keyWithoutVendorFormat + `)|(` + keyWithVendorFormat + `)` + valueFormat = `[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]` +) + +var keyValidationRegExp = regexp.MustCompile(`^(` + keyFormat + `)$`) +var valueValidationRegExp = regexp.MustCompile(`^(` + valueFormat + `)$`) + +// Tracestate represents tracing-system specific context in a list of key-value pairs. Tracestate allows different +// vendors propagate additional information and inter-operate with their legacy Id formats. +type Tracestate struct { + entries []Entry +} + +// Entry represents one key-value pair in a list of key-value pair of Tracestate. +type Entry struct { + // Key is an opaque string up to 256 characters printable. It MUST begin with a lowercase letter, + // and can only contain lowercase letters a-z, digits 0-9, underscores _, dashes -, asterisks *, and + // forward slashes /. + Key string + + // Value is an opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the + // range 0x20 to 0x7E) except comma , and =. + Value string +} + +// Entries returns a slice of Entry. +func (ts *Tracestate) Entries() []Entry { + if ts == nil { + return nil + } + return ts.entries +} + +func (ts *Tracestate) remove(key string) *Entry { + for index, entry := range ts.entries { + if entry.Key == key { + ts.entries = append(ts.entries[:index], ts.entries[index+1:]...) + return &entry + } + } + return nil +} + +func (ts *Tracestate) add(entries []Entry) error { + for _, entry := range entries { + ts.remove(entry.Key) + } + if len(ts.entries)+len(entries) > maxKeyValuePairs { + return fmt.Errorf("adding %d key-value pairs to current %d pairs exceeds the limit of %d", + len(entries), len(ts.entries), maxKeyValuePairs) + } + ts.entries = append(entries, ts.entries...) + return nil +} + +func isValid(entry Entry) bool { + return keyValidationRegExp.MatchString(entry.Key) && + valueValidationRegExp.MatchString(entry.Value) +} + +func containsDuplicateKey(entries ...Entry) (string, bool) { + keyMap := make(map[string]int) + for _, entry := range entries { + if _, ok := keyMap[entry.Key]; ok { + return entry.Key, true + } + keyMap[entry.Key] = 1 + } + return "", false +} + +func areEntriesValid(entries ...Entry) (*Entry, bool) { + for _, entry := range entries { + if !isValid(entry) { + return &entry, false + } + } + return nil, true +} + +// New creates a Tracestate object from a parent and/or entries (key-value pair). +// Entries from the parent are copied if present. The entries passed to this function +// are inserted in front of those copied from the parent. If an entry copied from the +// parent contains the same key as one of the entry in entries then the entry copied +// from the parent is removed. See add func. +// +// An error is returned with nil Tracestate if +// 1. one or more entry in entries is invalid. +// 2. two or more entries in the input entries have the same key. +// 3. the number of entries combined from the parent and the input entries exceeds maxKeyValuePairs. +// (duplicate entry is counted only once). +func New(parent *Tracestate, entries ...Entry) (*Tracestate, error) { + if parent == nil && len(entries) == 0 { + return nil, nil + } + if entry, ok := areEntriesValid(entries...); !ok { + return nil, fmt.Errorf("key-value pair {%s, %s} is invalid", entry.Key, entry.Value) + } + + if key, duplicate := containsDuplicateKey(entries...); duplicate { + return nil, fmt.Errorf("contains duplicate keys (%s)", key) + } + + tracestate := Tracestate{} + + if parent != nil && len(parent.entries) > 0 { + tracestate.entries = append([]Entry{}, parent.entries...) + } + + err := tracestate.add(entries) + if err != nil { + return nil, err + } + return &tracestate, nil +} diff --git a/vendor/go.opencensus.io/trace/tracestate/tracestate_test.go b/vendor/go.opencensus.io/trace/tracestate/tracestate_test.go new file mode 100644 index 0000000000..db76d9c3ff --- /dev/null +++ b/vendor/go.opencensus.io/trace/tracestate/tracestate_test.go @@ -0,0 +1,313 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tracestate + +import ( + "fmt" + "testing" +) + +func checkFront(t *testing.T, tracestate *Tracestate, wantKey, testname string) { + gotKey := tracestate.entries[0].Key + if gotKey != wantKey { + t.Errorf("test:%s: first entry in the list: got %q want %q", testname, gotKey, wantKey) + } +} + +func checkBack(t *testing.T, tracestate *Tracestate, wantKey, testname string) { + gotKey := tracestate.entries[len(tracestate.entries)-1].Key + if gotKey != wantKey { + t.Errorf("test:%s: last entry in the list: got %q want %q", testname, gotKey, wantKey) + } +} + +func checkSize(t *testing.T, tracestate *Tracestate, wantSize int, testname string) { + if gotSize := len(tracestate.entries); gotSize != wantSize { + t.Errorf("test:%s: size of the list: got %q want %q", testname, gotSize, wantSize) + } +} + +func (ts *Tracestate) get(key string) (string, bool) { + if ts == nil { + return "", false + } + for _, entry := range ts.entries { + if entry.Key == key { + return entry.Value, true + } + } + return "", false +} + +func checkKeyValue(t *testing.T, tracestate *Tracestate, key, wantValue, testname string) { + wantOk := true + if wantValue == "" { + wantOk = false + } + gotValue, gotOk := tracestate.get(key) + if wantOk != gotOk || gotValue != wantValue { + t.Errorf("test:%s: get value for key=%s failed: got %q want %q", testname, key, gotValue, wantValue) + } +} + +func checkError(t *testing.T, tracestate *Tracestate, err error, testname, msg string) { + if err != nil { + t.Errorf("test:%s: %s: tracestate=%v, error= %v", testname, msg, tracestate, err) + } +} + +func wantError(t *testing.T, tracestate *Tracestate, err error, testname, msg string) { + if err == nil { + t.Errorf("test:%s: %s: tracestate=%v, error=%v", testname, msg, tracestate, err) + } +} + +func TestCreateWithNullParent(t *testing.T) { + key1, value1 := "hello", "world" + testname := "TestCreateWithNullParent" + + entry := Entry{key1, value1} + tracestate, err := New(nil, entry) + checkError(t, tracestate, err, testname, "create failed from null parent") + checkKeyValue(t, tracestate, key1, value1, testname) +} + +func TestCreateFromParentWithSingleKey(t *testing.T) { + key1, value1, key2, value2 := "hello", "world", "foo", "bar" + testname := "TestCreateFromParentWithSingleKey" + + entry1 := Entry{key1, value1} + entry2 := Entry{key2, value2} + parent, _ := New(nil, entry1) + tracestate, err := New(parent, entry2) + + checkError(t, tracestate, err, testname, "create failed from parent with single key") + checkKeyValue(t, tracestate, key2, value2, testname) + checkFront(t, tracestate, key2, testname) + checkBack(t, tracestate, key1, testname) +} + +func TestCreateFromParentWithDoubleKeys(t *testing.T) { + key1, value1, key2, value2, key3, value3 := "hello", "world", "foo", "bar", "bar", "baz" + testname := "TestCreateFromParentWithDoubleKeys" + + entry1 := Entry{key1, value1} + entry2 := Entry{key2, value2} + entry3 := Entry{key3, value3} + parent, _ := New(nil, entry2, entry1) + tracestate, err := New(parent, entry3) + + checkError(t, tracestate, err, testname, "create failed from parent with double keys") + checkKeyValue(t, tracestate, key3, value3, testname) + checkFront(t, tracestate, key3, testname) + checkBack(t, tracestate, key1, testname) +} + +func TestCreateFromParentWithExistingKey(t *testing.T) { + key1, value1, key2, value2, key3, value3 := "hello", "world", "foo", "bar", "hello", "baz" + testname := "TestCreateFromParentWithExistingKey" + + entry1 := Entry{key1, value1} + entry2 := Entry{key2, value2} + entry3 := Entry{key3, value3} + parent, _ := New(nil, entry2, entry1) + tracestate, err := New(parent, entry3) + + checkError(t, tracestate, err, testname, "create failed with an existing key") + checkKeyValue(t, tracestate, key3, value3, testname) + checkFront(t, tracestate, key3, testname) + checkBack(t, tracestate, key2, testname) + checkSize(t, tracestate, 2, testname) +} + +func TestImplicitImmutableTracestate(t *testing.T) { + key1, value1, key2, value2, key3, value3 := "hello", "world", "hello", "bar", "foo", "baz" + testname := "TestImplicitImmutableTracestate" + + entry1 := Entry{key1, value1} + entry2 := Entry{key2, value2} + parent, _ := New(nil, entry1) + tracestate, err := New(parent, entry2) + + checkError(t, tracestate, err, testname, "create failed") + checkKeyValue(t, tracestate, key2, value2, testname) + checkKeyValue(t, parent, key2, value1, testname) + + // Get and update entries. + entries := tracestate.Entries() + entry := Entry{key3, value3} + entries = append(entries, entry) + + // Check Tracestate does not have key3. + checkKeyValue(t, tracestate, key3, "", testname) +} + +func TestKeyWithValidChar(t *testing.T) { + testname := "TestKeyWithValidChar" + + arrayRune := []rune("") + for c := 'a'; c <= 'z'; c++ { + arrayRune = append(arrayRune, c) + } + for c := '0'; c <= '9'; c++ { + arrayRune = append(arrayRune, c) + } + arrayRune = append(arrayRune, '_') + arrayRune = append(arrayRune, '-') + arrayRune = append(arrayRune, '*') + arrayRune = append(arrayRune, '/') + key := string(arrayRune) + entry := Entry{key, "world"} + tracestate, err := New(nil, entry) + + checkError(t, tracestate, err, testname, "create failed when the key contains all valid characters") +} + +func TestKeyWithInvalidChar(t *testing.T) { + testname := "TestKeyWithInvalidChar" + + keys := []string{"1ab", "1ab2", "Abc", " abc", "a=b"} + + for _, key := range keys { + entry := Entry{key, "world"} + tracestate, err := New(nil, entry) + wantError(t, tracestate, err, testname, fmt.Sprintf( + "create did not err with invalid key=%q", key)) + } +} + +func TestNilKey(t *testing.T) { + testname := "TestNilKey" + + entry := Entry{"", "world"} + tracestate, err := New(nil, entry) + wantError(t, tracestate, err, testname, "create did not err when the key is nil (\"\")") +} + +func TestValueWithInvalidChar(t *testing.T) { + testname := "TestValueWithInvalidChar" + + keys := []string{"A=B", "A,B", "AB "} + + for _, value := range keys { + entry := Entry{"hello", value} + tracestate, err := New(nil, entry) + wantError(t, tracestate, err, testname, + fmt.Sprintf("create did not err when the value is invalid (%q)", value)) + } +} + +func TestNilValue(t *testing.T) { + testname := "TestNilValue" + + tracestate, err := New(nil, Entry{"hello", ""}) + wantError(t, tracestate, err, testname, "create did not err when the value is nil (\"\")") +} + +func TestInvalidKeyLen(t *testing.T) { + testname := "TestInvalidKeyLen" + + arrayRune := []rune("") + for i := 0; i <= keyMaxSize+1; i++ { + arrayRune = append(arrayRune, 'a') + } + key := string(arrayRune) + tracestate, err := New(nil, Entry{key, "world"}) + + wantError(t, tracestate, err, testname, + fmt.Sprintf("create did not err when the length (%d) of the key is larger than max (%d)", + len(key), keyMaxSize)) +} + +func TestInvalidValueLen(t *testing.T) { + testname := "TestInvalidValueLen" + + arrayRune := []rune("") + for i := 0; i <= valueMaxSize+1; i++ { + arrayRune = append(arrayRune, 'a') + } + value := string(arrayRune) + tracestate, err := New(nil, Entry{"hello", value}) + + wantError(t, tracestate, err, testname, + fmt.Sprintf("create did not err when the length (%d) of the value is larger than max (%d)", + len(value), valueMaxSize)) +} + +func TestCreateFromArrayWithOverLimitKVPairs(t *testing.T) { + testname := "TestCreateFromArrayWithOverLimitKVPairs" + + entries := []Entry{} + for i := 0; i <= maxKeyValuePairs; i++ { + key := fmt.Sprintf("a%db", i) + entry := Entry{key, "world"} + entries = append(entries, entry) + } + tracestate, err := New(nil, entries...) + wantError(t, tracestate, err, testname, + fmt.Sprintf("create did not err when the number (%d) of key-value pairs is larger than max (%d)", + len(entries), maxKeyValuePairs)) +} + +func TestCreateFromEmptyArray(t *testing.T) { + testname := "TestCreateFromEmptyArray" + + tracestate, err := New(nil, nil...) + checkError(t, tracestate, err, testname, + "failed to create nil tracestate") +} + +func TestCreateFromParentWithOverLimitKVPairs(t *testing.T) { + testname := "TestCreateFromParentWithOverLimitKVPairs" + + entries := []Entry{} + for i := 0; i < maxKeyValuePairs; i++ { + key := fmt.Sprintf("a%db", i) + entry := Entry{key, "world"} + entries = append(entries, entry) + } + parent, err := New(nil, entries...) + + checkError(t, parent, err, testname, fmt.Sprintf("create failed to add %d key-value pair", maxKeyValuePairs)) + + // Add one more to go over the limit + key := fmt.Sprintf("a%d", maxKeyValuePairs) + tracestate, err := New(parent, Entry{key, "world"}) + wantError(t, tracestate, err, testname, + fmt.Sprintf("create did not err when attempted to exceed the key-value pair limit of %d", maxKeyValuePairs)) +} + +func TestCreateFromArrayWithDuplicateKeys(t *testing.T) { + key1, value1, key2, value2, key3, value3 := "hello", "world", "foo", "bar", "hello", "baz" + testname := "TestCreateFromArrayWithDuplicateKeys" + + entry1 := Entry{key1, value1} + entry2 := Entry{key2, value2} + entry3 := Entry{key3, value3} + tracestate, err := New(nil, entry1, entry2, entry3) + + wantError(t, tracestate, err, testname, + "create did not err when entries contained duplicate keys") +} + +func TestEntriesWithNil(t *testing.T) { + ts, err := New(nil) + if err != nil { + t.Fatal(err) + } + if got, want := len(ts.Entries()), 0; got != want { + t.Errorf("zero value should have no entries, got %v; want %v", got, want) + } +} diff --git a/vendor/go.opencensus.io/zpages/example_test.go b/vendor/go.opencensus.io/zpages/example_test.go new file mode 100644 index 0000000000..141663cc4c --- /dev/null +++ b/vendor/go.opencensus.io/zpages/example_test.go @@ -0,0 +1,28 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zpages_test + +import ( + "log" + "net/http" + + "go.opencensus.io/zpages" +) + +func Example() { + // Both /debug/tracez and /debug/rpcz will be served on the default mux. + zpages.Handle(nil, "/debug") + log.Fatal(http.ListenAndServe("127.0.0.1:9999", nil)) +} diff --git a/vendor/go.opencensus.io/zpages/formatter_test.go b/vendor/go.opencensus.io/zpages/formatter_test.go new file mode 100644 index 0000000000..92f5a8f4dd --- /dev/null +++ b/vendor/go.opencensus.io/zpages/formatter_test.go @@ -0,0 +1,42 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package zpages + +import "testing" + +func TestCountFormatter(t *testing.T) { + tests := []struct { + in uint64 + want string + }{ + {0, " "}, + {1, "1"}, + {1024, "1024"}, + {1e5, "100000"}, + {1e6, "1.000 M "}, + {1e9, "1.000 G "}, + {1e8 + 2e9, "2.100 G "}, + {1e12, "1.000 T "}, + {1e15, "1.000 P "}, + {1e18, "1.000 E "}, + } + + for _, tt := range tests { + if g, w := countFormatter(tt.in), tt.want; g != w { + t.Errorf("%d got %q want %q", tt.in, g, w) + } + } +} diff --git a/vendor/go.opencensus.io/zpages/internal/gen.go b/vendor/go.opencensus.io/zpages/internal/gen.go new file mode 100644 index 0000000000..453e217544 --- /dev/null +++ b/vendor/go.opencensus.io/zpages/internal/gen.go @@ -0,0 +1,19 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package internal // import "go.opencensus.io/zpages/internal" + +// go get https://github.com/mjibson/esc.git +//go:generate esc -pkg internal -o resources.go public/ templates/ diff --git a/vendor/go.opencensus.io/zpages/internal/public/opencensus.css b/vendor/go.opencensus.io/zpages/internal/public/opencensus.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vendor/go.opencensus.io/zpages/internal/resources.go b/vendor/go.opencensus.io/zpages/internal/resources.go new file mode 100644 index 0000000000..5b7fc76b29 --- /dev/null +++ b/vendor/go.opencensus.io/zpages/internal/resources.go @@ -0,0 +1,284 @@ +// Code generated by "esc -pkg resources -o resources.go public/ templates/"; DO NOT EDIT. + +package internal + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "io/ioutil" + "net/http" + "os" + "path" + "sync" + "time" +) + +type _escLocalFS struct{} + +var _escLocal _escLocalFS + +type _escStaticFS struct{} + +var _escStatic _escStaticFS + +type _escDirectory struct { + fs http.FileSystem + name string +} + +type _escFile struct { + compressed string + size int64 + modtime int64 + local string + isDir bool + + once sync.Once + data []byte + name string +} + +func (_escLocalFS) Open(name string) (http.File, error) { + f, present := _escData[path.Clean(name)] + if !present { + return nil, os.ErrNotExist + } + return os.Open(f.local) +} + +func (_escStaticFS) prepare(name string) (*_escFile, error) { + f, present := _escData[path.Clean(name)] + if !present { + return nil, os.ErrNotExist + } + var err error + f.once.Do(func() { + f.name = path.Base(name) + if f.size == 0 { + return + } + var gr *gzip.Reader + b64 := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(f.compressed)) + gr, err = gzip.NewReader(b64) + if err != nil { + return + } + f.data, err = ioutil.ReadAll(gr) + }) + if err != nil { + return nil, err + } + return f, nil +} + +func (fs _escStaticFS) Open(name string) (http.File, error) { + f, err := fs.prepare(name) + if err != nil { + return nil, err + } + return f.File() +} + +func (dir _escDirectory) Open(name string) (http.File, error) { + return dir.fs.Open(dir.name + name) +} + +func (f *_escFile) File() (http.File, error) { + type httpFile struct { + *bytes.Reader + *_escFile + } + return &httpFile{ + Reader: bytes.NewReader(f.data), + _escFile: f, + }, nil +} + +func (f *_escFile) Close() error { + return nil +} + +func (f *_escFile) Readdir(count int) ([]os.FileInfo, error) { + return nil, nil +} + +func (f *_escFile) Stat() (os.FileInfo, error) { + return f, nil +} + +func (f *_escFile) Name() string { + return f.name +} + +func (f *_escFile) Size() int64 { + return f.size +} + +func (f *_escFile) Mode() os.FileMode { + return 0 +} + +func (f *_escFile) ModTime() time.Time { + return time.Unix(f.modtime, 0) +} + +func (f *_escFile) IsDir() bool { + return f.isDir +} + +func (f *_escFile) Sys() interface{} { + return f +} + +// FS returns a http.Filesystem for the embedded assets. If useLocal is true, +// the filesystem's contents are instead used. +func FS(useLocal bool) http.FileSystem { + if useLocal { + return _escLocal + } + return _escStatic +} + +// Dir returns a http.Filesystem for the embedded assets on a given prefix dir. +// If useLocal is true, the filesystem's contents are instead used. +func Dir(useLocal bool, name string) http.FileSystem { + if useLocal { + return _escDirectory{fs: _escLocal, name: name} + } + return _escDirectory{fs: _escStatic, name: name} +} + +// FSByte returns the named file from the embedded assets. If useLocal is +// true, the filesystem's contents are instead used. +func FSByte(useLocal bool, name string) ([]byte, error) { + if useLocal { + f, err := _escLocal.Open(name) + if err != nil { + return nil, err + } + b, err := ioutil.ReadAll(f) + _ = f.Close() + return b, err + } + f, err := _escStatic.prepare(name) + if err != nil { + return nil, err + } + return f.data, nil +} + +// FSMustByte is the same as FSByte, but panics if name is not present. +func FSMustByte(useLocal bool, name string) []byte { + b, err := FSByte(useLocal, name) + if err != nil { + panic(err) + } + return b +} + +// FSString is the string version of FSByte. +func FSString(useLocal bool, name string) (string, error) { + b, err := FSByte(useLocal, name) + return string(b), err +} + +// FSMustString is the string version of FSMustByte. +func FSMustString(useLocal bool, name string) string { + return string(FSMustByte(useLocal, name)) +} + +var _escData = map[string]*_escFile{ + + "/public/opencensus.css": { + local: "public/opencensus.css", + size: 0, + modtime: 1519153040, + compressed: ` +H4sIAAAAAAAC/wEAAP//AAAAAAAAAAA= +`, + }, + + "/templates/footer.html": { + local: "templates/footer.html", + size: 16, + modtime: 1519153248, + compressed: ` +H4sIAAAAAAAC/7LRT8pPqbTjstHPKMnNseMCBAAA//8ATCBFEAAAAA== +`, + }, + + "/templates/header.html": { + local: "templates/header.html", + size: 523, + modtime: 1519164535, + compressed: ` +H4sIAAAAAAAC/5TRv07rMBQG8D1P4ev1qvat7oKQEwZgYEAwdGF0nZP4UP+JfE6oqqrvjkyKBGIpky0f ++6fP+syfu6fbzcvzvfAcQ9eYuohg09hKSLIzHmzfNUIIYSKwFc7bQsCtnHlYXcnziJEDdMej2tTN6WT0 +crJMA6adKBBaST4XdjMLdDlJ4QsMrdR6v9+rPEFykGgmhVkP9q1eUeiy1D8ZPgQgD8CfxjRvAzr9BXFE +F730zBNdaz3kxKTGnMcAdkJSLkddM9wMNmI4tI+WoaANfx9cTiR/QbvcgxqBYx/q39bqv/qn45lTmHoc +82rCtFMR00fwM06u4MSihwGKoOIuJSvzSrIzehG6xuilSLPN/aHWvP7Wll93zXsAAAD//6iqQ1ULAgAA +`, + }, + + "/templates/rpcz.html": { + local: "templates/rpcz.html", + size: 2626, + modtime: 1519164559, + compressed: ` +H4sIAAAAAAAC/+yW3WrbMBTH7/0UwmUjYyxJU3o1W1C6sQ4WNrq+gCwdfzBFMtJx9+Hl3cex3DhNCrOz +XfbGxFZ+5/8D+Ry5bZ0wBbD5VxT4wdmm9tttlNQ8QZFpYFkhrbYuPQMAyHP2vVJYpufL5QueoGNCV4VJ +JRgExxNUPMmtQearX5C+XvG2nb+rHEisrNlukwUt8mRB/1ugowuF8GRR8+ggMD7L8/wSIGa5ExtIM/uD +SdDa10JWpkiX3V0tlKK7FY8ixhgjp6ECAFwqiHm3FJZLCi2DKnnsLzGphfdprM9jJi0lmfSCX9vG4FTo +6r5gWiAY+ZPNNv7VVP5WILCZq+ViOvvR1A2y2bfsBPZzg6fD752zzndU2Aza47H70r9KGnLka8DSql38 +S5P5+u3x9Vgr1HBVUSJfV2bel3i8cOOefn5ncf6c+Zz5XzKfaADyGLrlYn9UvlnxB52DERlFw4Q2oval +RRrQDyX3zBVPMhq4oXlo2mZHjXvcyqrXjzv/mAp0A29dmQbht6TfVGscdWMbN5W5syj0I2ik59V98SmM +2F5240elDlynO5kKwjtspO3tl2sa6r2qEwijYnusM50KBdE9aqRqd4DsySqBYnT2Du6UT0OD+AE7Uj6c +YKfaD/R0/YH9F/9wiE5uv4BN7L8A/a0BwxxqWzCKPg37b7bdgz8BAAD//6NjPmJCCgAA +`, + }, + + "/templates/summary.html": { + local: "templates/summary.html", + size: 1619, + modtime: 1519164559, + compressed: ` +H4sIAAAAAAAC/6yVPW/bMBCG9/yKg2p4qu2kW12JRQtkCzok3YoOlHSWBdMngaSc2iz/e8EP+Stqi8Re +DIo63t3zvjwr1TwXCEpvBWZJ3sgS5US1vKipmsNtwm4AAFItwyI8lFA0QrWcsjvgoq4oE7jQLM3ZU8sJ +vvE1prOcpTNdnhxjY8pV+yn8/j5+8KFDiZMCSaNMXPLHjqim6i2pB5v/OFDjgWukYgtPfN0KVFerNcRz +L2Ujhyuls17xv0t/pcbelsYYyalCmEbBvnbFCrVzXlmb6uU/wX8YM7X2Z0ReMmOQSmuviRIENGbEYZ7B +9LvkBap7KtumJm2teyNqWin/9sGt/GaAGsnmuaYSf733Sx/z2DyHkAmMiK/RbzreuFkvADdIh7NOBrkf +LF6sKtl0VM7hHSImjlko9EGBHyZRAUdvTMzdD8b/9IgtRKijVC/k57CUuMgSp421n3dOOgeUGePBrB3v +9LbF7NY1Of1S6HrjG+HsUMr1ft7wIXIfdUb1aoa9Ib0bGy66IH28d07ACxjvxjvV5X5pzCj65rhDpSPs +/o6e0J9Pge+G+dv98tClYlxs6IcDbPDW/wGpE8cGfB2Iiij9kHnIdOY/JezmTwAAAP//Dz6TJ1MGAAA= +`, + }, + + "/templates/traces.html": { + local: "templates/traces.html", + size: 420, + modtime: 1519164578, + compressed: ` +H4sIAAAAAAAC/4yQsU70MBCEez/FKtIv3RW/w6WgOIw7kGgoDiRqO14gwnGM1xEgs++OnKMA5Qq2ssYz +I82nolZW30UT4NaMuIdSZH0wg2qtVm3UQkVd1XlkhgO+zkiZvj8SavHwjAFO35U3kdDBhrDfiv9/PFFK +MuEJQR6mN2IuJaYh5Edo/nXn1MBmCA7fQV4P6B3B2ZYZfnh23dqzO3p+i12tlp85mR4HxyxKweCYVbvs +UjYt25UFyh8eL5t+8lPaWz/jRaPva+zGVUowogkEZMbo0UE6MpKiIlinTf9yMh6mvKpYMH8FAAD//yQs +JUakAQAA +`, + }, + + "/": { + isDir: true, + local: "", + }, + + "/public": { + isDir: true, + local: "public", + }, + + "/templates": { + isDir: true, + local: "templates", + }, +} diff --git a/vendor/go.opencensus.io/zpages/internal/templates/footer.html b/vendor/go.opencensus.io/zpages/internal/templates/footer.html new file mode 100644 index 0000000000..308b1d01b6 --- /dev/null +++ b/vendor/go.opencensus.io/zpages/internal/templates/footer.html @@ -0,0 +1,2 @@ + + diff --git a/vendor/go.opencensus.io/zpages/internal/templates/header.html b/vendor/go.opencensus.io/zpages/internal/templates/header.html new file mode 100644 index 0000000000..9884d71a5a --- /dev/null +++ b/vendor/go.opencensus.io/zpages/internal/templates/header.html @@ -0,0 +1,12 @@ + + + + {{.Title}} + + + + + + + +

{{.Title}}

diff --git a/vendor/go.opencensus.io/zpages/internal/templates/rpcz.html b/vendor/go.opencensus.io/zpages/internal/templates/rpcz.html new file mode 100644 index 0000000000..a01b30b73a --- /dev/null +++ b/vendor/go.opencensus.io/zpages/internal/templates/rpcz.html @@ -0,0 +1,52 @@ +{{range .StatGroups}} +

{{.Direction}}

+ + + + + + + + + + + + + + + + + + + + + + + + +{{range .Snapshots}} + + + + + + + + + + + + + + + + + + + + + + +{{end}} +
CountAvg latency (ms)Rate (rpc/s)Input (kb/s)Output (kb/s)Errors
Method    Min.Hr.Tot.    Min.Hr.Tot.    Min.Hr.Tot.    Min.Hr.Tot.    Min.Hr.Tot.    Min.Hr.Tot.
 
{{.Method}}{{.CountMinute|count}}{{.CountHour|count}}{{.CountTotal|count}}{{.AvgLatencyMinute|ms}}{{.AvgLatencyHour|ms}}{{.AvgLatencyTotal|ms}}{{.RPCRateMinute|rate}}{{.RPCRateHour|rate}}{{.RPCRateTotal|rate}}{{.InputRateMinute|datarate}}{{.InputRateHour|datarate}}{{.InputRateTotal|datarate}}{{.OutputRateMinute|datarate}}{{.OutputRateHour|datarate}}{{.OutputRateTotal|datarate}}{{.ErrorsMinute|count}}{{.ErrorsHour|count}}{{.ErrorsTotal|count}}
+{{end}} diff --git a/vendor/go.opencensus.io/zpages/internal/templates/summary.html b/vendor/go.opencensus.io/zpages/internal/templates/summary.html new file mode 100644 index 0000000000..8521a8bdc6 --- /dev/null +++ b/vendor/go.opencensus.io/zpages/internal/templates/summary.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + {{range .LatencyBucketNames}}{{end}} + + + +{{$a := .TracesEndpoint}} +{{$links := .Links}} +{{range $rowindex, $row := .Rows}} +{{- $name := .Name}} +{{- if even $rowindex}}{{else}}{{end -}} + +{{- if $links -}} + +{{- else -}} + +{{- end -}} + +{{- if $links -}} +{{range $index, $value := .Latency}}{{end}} +{{- else -}} +{{range .Latency}}{{end}} +{{- end -}} + +{{- if $links -}} + +{{- else -}} + +{{- end -}} + +{{end}}
Span Name  |  Running  |  Latency Samples  |  Error Samples
  |    |  [{{.}}]  |  
{{.Name}}  |  {{.Active}}{{.Active}}  |  {{$value}}{{.}}  |  {{.Errors}}{{.Errors}}
diff --git a/vendor/go.opencensus.io/zpages/internal/templates/traces.html b/vendor/go.opencensus.io/zpages/internal/templates/traces.html new file mode 100644 index 0000000000..b5995dca9f --- /dev/null +++ b/vendor/go.opencensus.io/zpages/internal/templates/traces.html @@ -0,0 +1,10 @@ +

Span Name: {{.Name}}

+

{{.Num}} Requests

+
+When                       Elapsed (sec)
+----------------------------------------
+{{range .Rows}}{{printf "%26s" (index .Fields 0)}} {{printf "%12s" (index .Fields 1)}} {{index .Fields 2}}{{.|traceid}}
+{{end}}
+
+

TraceId means sampled request. + TraceId means not sampled request.

diff --git a/vendor/go.opencensus.io/zpages/rpcz.go b/vendor/go.opencensus.io/zpages/rpcz.go new file mode 100644 index 0000000000..dee28f9829 --- /dev/null +++ b/vendor/go.opencensus.io/zpages/rpcz.go @@ -0,0 +1,333 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package zpages + +import ( + "fmt" + "io" + "log" + "math" + "net/http" + "sort" + "sync" + "text/tabwriter" + "time" + + "go.opencensus.io/plugin/ocgrpc" + "go.opencensus.io/stats/view" +) + +const bytesPerKb = 1024 + +var ( + programStartTime = time.Now() + mu sync.Mutex // protects snaps + snaps = make(map[methodKey]*statSnapshot) + + // viewType lists the views we are interested in for RPC stats. + // A view's map value indicates whether that view contains data for received + // RPCs. + viewType = map[*view.View]bool{ + ocgrpc.ClientCompletedRPCsView: false, + ocgrpc.ClientSentBytesPerRPCView: false, + ocgrpc.ClientSentMessagesPerRPCView: false, + ocgrpc.ClientReceivedBytesPerRPCView: false, + ocgrpc.ClientReceivedMessagesPerRPCView: false, + ocgrpc.ClientRoundtripLatencyView: false, + ocgrpc.ServerCompletedRPCsView: true, + ocgrpc.ServerReceivedBytesPerRPCView: true, + ocgrpc.ServerReceivedMessagesPerRPCView: true, + ocgrpc.ServerSentBytesPerRPCView: true, + ocgrpc.ServerSentMessagesPerRPCView: true, + ocgrpc.ServerLatencyView: true, + } +) + +func registerRPCViews() { + views := make([]*view.View, 0, len(viewType)) + for v := range viewType { + views = append(views, v) + } + if err := view.Register(views...); err != nil { + log.Printf("error subscribing to views: %v", err) + } + view.RegisterExporter(snapExporter{}) +} + +func rpczHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + WriteHTMLRpczPage(w) +} + +// WriteHTMLRpczPage writes an HTML document to w containing per-method RPC stats. +func WriteHTMLRpczPage(w io.Writer) { + if err := headerTemplate.Execute(w, headerData{Title: "RPC Stats"}); err != nil { + log.Printf("zpages: executing template: %v", err) + } + WriteHTMLRpczSummary(w) + if err := footerTemplate.Execute(w, nil); err != nil { + log.Printf("zpages: executing template: %v", err) + } +} + +// WriteHTMLRpczSummary writes HTML to w containing per-method RPC stats. +// +// It includes neither a header nor footer, so you can embed this data in other pages. +func WriteHTMLRpczSummary(w io.Writer) { + mu.Lock() + if err := statsTemplate.Execute(w, getStatsPage()); err != nil { + log.Printf("zpages: executing template: %v", err) + } + mu.Unlock() +} + +// WriteTextRpczPage writes formatted text to w containing per-method RPC stats. +func WriteTextRpczPage(w io.Writer) { + mu.Lock() + defer mu.Unlock() + page := getStatsPage() + + for i, sg := range page.StatGroups { + switch i { + case 0: + fmt.Fprint(w, "Sent:\n") + case 1: + fmt.Fprint(w, "\nReceived:\n") + } + tw := tabwriter.NewWriter(w, 6, 8, 1, ' ', 0) + fmt.Fprint(tw, "Method\tCount\t\t\tAvgLat\t\t\tMaxLat\t\t\tRate\t\t\tIn (MiB/s)\t\t\tOut (MiB/s)\t\t\tErrors\t\t\n") + fmt.Fprint(tw, "\tMin\tHr\tTot\tMin\tHr\tTot\tMin\tHr\tTot\tMin\tHr\tTot\tMin\tHr\tTot\tMin\tHr\tTot\tMin\tHr\tTot\n") + for _, s := range sg.Snapshots { + fmt.Fprintf(tw, "%s\t%d\t%d\t%d\t%v\t%v\t%v\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\t%d\t%d\t%d\n", + s.Method, + s.CountMinute, + s.CountHour, + s.CountTotal, + s.AvgLatencyMinute, + s.AvgLatencyHour, + s.AvgLatencyTotal, + s.RPCRateMinute, + s.RPCRateHour, + s.RPCRateTotal, + s.InputRateMinute/bytesPerKb, + s.InputRateHour/bytesPerKb, + s.InputRateTotal/bytesPerKb, + s.OutputRateMinute/bytesPerKb, + s.OutputRateHour/bytesPerKb, + s.OutputRateTotal/bytesPerKb, + s.ErrorsMinute, + s.ErrorsHour, + s.ErrorsTotal) + } + tw.Flush() + } +} + +// headerData contains data for the header template. +type headerData struct { + Title string +} + +// statsPage aggregates stats on the page for 'sent' and 'received' categories +type statsPage struct { + StatGroups []*statGroup +} + +// statGroup aggregates snapshots for a directional category +type statGroup struct { + Direction string + Snapshots []*statSnapshot +} + +func (s *statGroup) Len() int { + return len(s.Snapshots) +} + +func (s *statGroup) Swap(i, j int) { + s.Snapshots[i], s.Snapshots[j] = s.Snapshots[j], s.Snapshots[i] +} + +func (s *statGroup) Less(i, j int) bool { + return s.Snapshots[i].Method < s.Snapshots[j].Method +} + +// statSnapshot holds the data items that are presented in a single row of RPC +// stat information. +type statSnapshot struct { + // TODO: compute hour/minute values from cumulative + Method string + Received bool + CountMinute uint64 + CountHour uint64 + CountTotal uint64 + AvgLatencyMinute time.Duration + AvgLatencyHour time.Duration + AvgLatencyTotal time.Duration + RPCRateMinute float64 + RPCRateHour float64 + RPCRateTotal float64 + InputRateMinute float64 + InputRateHour float64 + InputRateTotal float64 + OutputRateMinute float64 + OutputRateHour float64 + OutputRateTotal float64 + ErrorsMinute uint64 + ErrorsHour uint64 + ErrorsTotal uint64 +} + +type methodKey struct { + method string + received bool +} + +type snapExporter struct{} + +func (s snapExporter) ExportView(vd *view.Data) { + received, ok := viewType[vd.View] + if !ok { + return + } + if len(vd.Rows) == 0 { + return + } + ageSec := float64(time.Now().Sub(programStartTime)) / float64(time.Second) + + computeRate := func(maxSec, x float64) float64 { + dur := ageSec + if maxSec > 0 && dur > maxSec { + dur = maxSec + } + return x / dur + } + + convertTime := func(ms float64) time.Duration { + if math.IsInf(ms, 0) || math.IsNaN(ms) { + return 0 + } + return time.Duration(float64(time.Millisecond) * ms) + } + + haveResetErrors := make(map[string]struct{}) + + mu.Lock() + defer mu.Unlock() + for _, row := range vd.Rows { + var method string + for _, tag := range row.Tags { + if tag.Key == ocgrpc.KeyClientMethod || tag.Key == ocgrpc.KeyServerMethod { + method = tag.Value + break + } + } + + key := methodKey{method: method, received: received} + s := snaps[key] + if s == nil { + s = &statSnapshot{Method: method, Received: received} + snaps[key] = s + } + + var ( + sum float64 + count float64 + ) + switch v := row.Data.(type) { + case *view.CountData: + sum = float64(v.Value) + count = float64(v.Value) + case *view.DistributionData: + sum = v.Sum() + count = float64(v.Count) + case *view.SumData: + sum = v.Value + count = v.Value + } + + // Update field of s corresponding to the view. + switch vd.View { + case ocgrpc.ClientCompletedRPCsView: + if _, ok := haveResetErrors[method]; !ok { + haveResetErrors[method] = struct{}{} + s.ErrorsTotal = 0 + } + for _, tag := range row.Tags { + if tag.Key == ocgrpc.KeyClientStatus && tag.Value != "OK" { + s.ErrorsTotal += uint64(count) + } + } + + case ocgrpc.ClientRoundtripLatencyView: + s.AvgLatencyTotal = convertTime(sum / count) + + case ocgrpc.ClientSentBytesPerRPCView: + s.OutputRateTotal = computeRate(0, sum) + + case ocgrpc.ClientReceivedBytesPerRPCView: + s.InputRateTotal = computeRate(0, sum) + + case ocgrpc.ClientSentMessagesPerRPCView: + s.CountTotal = uint64(count) + s.RPCRateTotal = computeRate(0, count) + + case ocgrpc.ClientReceivedMessagesPerRPCView: + // currently unused + + case ocgrpc.ServerCompletedRPCsView: + if _, ok := haveResetErrors[method]; !ok { + haveResetErrors[method] = struct{}{} + s.ErrorsTotal = 0 + } + for _, tag := range row.Tags { + if tag.Key == ocgrpc.KeyServerStatus && tag.Value != "OK" { + s.ErrorsTotal += uint64(count) + } + } + + case ocgrpc.ServerLatencyView: + s.AvgLatencyTotal = convertTime(sum / count) + + case ocgrpc.ServerSentBytesPerRPCView: + s.OutputRateTotal = computeRate(0, sum) + + case ocgrpc.ServerReceivedMessagesPerRPCView: + s.CountTotal = uint64(count) + s.RPCRateTotal = computeRate(0, count) + + case ocgrpc.ServerSentMessagesPerRPCView: + // currently unused + } + } +} + +func getStatsPage() *statsPage { + sentStats := statGroup{Direction: "Sent"} + receivedStats := statGroup{Direction: "Received"} + for key, sg := range snaps { + if key.received { + receivedStats.Snapshots = append(receivedStats.Snapshots, sg) + } else { + sentStats.Snapshots = append(sentStats.Snapshots, sg) + } + } + sort.Sort(&sentStats) + sort.Sort(&receivedStats) + + return &statsPage{ + StatGroups: []*statGroup{&sentStats, &receivedStats}, + } +} diff --git a/vendor/go.opencensus.io/zpages/rpcz_test.go b/vendor/go.opencensus.io/zpages/rpcz_test.go new file mode 100644 index 0000000000..5a5f13b65d --- /dev/null +++ b/vendor/go.opencensus.io/zpages/rpcz_test.go @@ -0,0 +1,55 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package zpages + +import ( + "context" + "testing" + "time" + + "go.opencensus.io/internal/testpb" + "go.opencensus.io/stats/view" +) + +func TestRpcz(t *testing.T) { + client, cleanup := testpb.NewTestClient(t) + defer cleanup() + + _, err := client.Single(context.Background(), &testpb.FooRequest{}) + if err != nil { + t.Fatal(err) + } + + view.SetReportingPeriod(time.Millisecond) + time.Sleep(2 * time.Millisecond) + view.SetReportingPeriod(time.Second) + + mu.Lock() + defer mu.Unlock() + + if len(snaps) == 0 { + t.Fatal("Expected len(snaps) > 0") + } + + snapshot, ok := snaps[methodKey{"testpb.Foo/Single", false}] + if !ok { + t.Fatal("Expected method stats not recorded") + } + + if got, want := snapshot.CountTotal, uint64(1); got != want { + t.Errorf("snapshot.CountTotal = %d; want %d", got, want) + } +} diff --git a/vendor/go.opencensus.io/zpages/templates.go b/vendor/go.opencensus.io/zpages/templates.go new file mode 100644 index 0000000000..6675b0ab08 --- /dev/null +++ b/vendor/go.opencensus.io/zpages/templates.go @@ -0,0 +1,125 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package zpages + +import ( + "fmt" + "html/template" + "io/ioutil" + "log" + "strconv" + "time" + + "go.opencensus.io/trace" + "go.opencensus.io/zpages/internal" +) + +var ( + fs = internal.FS(false) + templateFunctions = template.FuncMap{ + "count": countFormatter, + "ms": msFormatter, + "rate": rateFormatter, + "datarate": dataRateFormatter, + "even": even, + "traceid": traceIDFormatter, + } + headerTemplate = parseTemplate("header") + summaryTableTemplate = parseTemplate("summary") + statsTemplate = parseTemplate("rpcz") + tracesTableTemplate = parseTemplate("traces") + footerTemplate = parseTemplate("footer") +) + +func parseTemplate(name string) *template.Template { + f, err := fs.Open("/templates/" + name + ".html") + if err != nil { + log.Panicf("%v: %v", name, err) + } + defer f.Close() + text, err := ioutil.ReadAll(f) + if err != nil { + log.Panicf("%v: %v", name, err) + } + return template.Must(template.New(name).Funcs(templateFunctions).Parse(string(text))) +} + +func countFormatter(num uint64) string { + if num <= 0 { + return " " + } + var floatVal float64 + var suffix string + + if num >= 1e18 { + floatVal = float64(num) / 1e18 + suffix = " E " + } else if num >= 1e15 { + floatVal = float64(num) / 1e15 + suffix = " P " + } else if num >= 1e12 { + floatVal = float64(num) / 1e12 + suffix = " T " + } else if num >= 1e9 { + floatVal = float64(num) / 1e9 + suffix = " G " + } else if num >= 1e6 { + floatVal = float64(num) / 1e6 + suffix = " M " + } + + if floatVal != 0 { + return fmt.Sprintf("%1.3f%s", floatVal, suffix) + } + return fmt.Sprint(num) +} + +func msFormatter(d time.Duration) string { + if d == 0 { + return "0" + } + if d < 10*time.Millisecond { + return fmt.Sprintf("%.3f", float64(d)*1e-6) + } + return strconv.Itoa(int(d / time.Millisecond)) +} + +func rateFormatter(r float64) string { + return fmt.Sprintf("%.3f", r) +} + +func dataRateFormatter(b float64) string { + return fmt.Sprintf("%.3f", b/1e6) +} + +func traceIDFormatter(r traceRow) template.HTML { + sc := r.SpanContext + if sc == (trace.SpanContext{}) { + return "" + } + col := "black" + if sc.TraceOptions.IsSampled() { + col = "blue" + } + if r.ParentSpanID != (trace.SpanID{}) { + return template.HTML(fmt.Sprintf(`trace_id: %s span_id: %s parent_span_id: %s`, col, sc.TraceID, sc.SpanID, r.ParentSpanID)) + } + return template.HTML(fmt.Sprintf(`trace_id: %s span_id: %s`, col, sc.TraceID, sc.SpanID)) +} + +func even(x int) bool { + return x%2 == 0 +} diff --git a/vendor/go.opencensus.io/zpages/templates_test.go b/vendor/go.opencensus.io/zpages/templates_test.go new file mode 100644 index 0000000000..e3adf6cf51 --- /dev/null +++ b/vendor/go.opencensus.io/zpages/templates_test.go @@ -0,0 +1,99 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zpages + +import ( + "bytes" + "html/template" + "testing" +) + +const tmplBody = ` + {{.Method}} + + {{.CountMinute|count}} + {{.CountHour|count}} + {{.CountTotal|count}} + {{.AvgLatencyMinute|ms}} + {{.AvgLatencyHour|ms}} + {{.AvgLatencyTotal|ms}} + {{.RPCRateMinute|rate}} + {{.RPCRateHour|rate}} + {{.RPCRateTotal|rate}} + {{.InputRateMinute|datarate}} + {{.InputRateHour|datarate}} + {{.InputRateTotal|datarate}} + {{.OutputRateMinute|datarate}} + {{.OutputRateHour|datarate}} + {{.OutputRateTotal|datarate}} + {{.ErrorsMinute|count}} + {{.ErrorsHour|count}} + {{.ErrorsTotal|count}} +` + +var tmpl = template.Must(template.New("countTest").Funcs(templateFunctions).Parse(tmplBody)) + +func TestTemplateFuncs(t *testing.T) { + buf := new(bytes.Buffer) + sshot := &statSnapshot{ + Method: "Foo", + CountMinute: 1e9, + CountHour: 5000, + CountTotal: 1e12, + AvgLatencyMinute: 10000, + AvgLatencyHour: 1000, + AvgLatencyTotal: 20000, + RPCRateMinute: 2000, + RPCRateHour: 5000, + RPCRateTotal: 75000, + InputRateMinute: 75000, + InputRateHour: 75000, + InputRateTotal: 75000, + OutputRateMinute: 75000, + OutputRateHour: 75000, + OutputRateTotal: 75000, + ErrorsMinute: 120000000, + ErrorsHour: 75000000, + ErrorsTotal: 7500000, + } + if err := tmpl.Execute(buf, sshot); err != nil { + t.Fatalf("Failed to execute template: %v", err) + } + want := ` + Foo + + 1.000 G + 5000 + 1.000 T + 0.010 + 0.001 + 0.020 + 2000.000 + 5000.000 + 75000.000 + 0.075 + 0.075 + 0.075 + 0.075 + 0.075 + 0.075 + 120.000 M + 75.000 M + 7.500 M +` + if g, w := buf.String(), want; g != w { + t.Errorf("Output mismatch:\nGot:\n\t%s\nWant:\n\t%s", g, w) + } +} diff --git a/vendor/go.opencensus.io/zpages/tracez.go b/vendor/go.opencensus.io/zpages/tracez.go new file mode 100644 index 0000000000..330022c23e --- /dev/null +++ b/vendor/go.opencensus.io/zpages/tracez.go @@ -0,0 +1,442 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package zpages + +import ( + "fmt" + "io" + "log" + "net/http" + "sort" + "strconv" + "strings" + "text/tabwriter" + "time" + + "go.opencensus.io/internal" + "go.opencensus.io/trace" +) + +const ( + // spanNameQueryField is the header for span name. + spanNameQueryField = "zspanname" + // spanTypeQueryField is the header for type (running = 0, latency = 1, error = 2) to display. + spanTypeQueryField = "ztype" + // spanSubtypeQueryField is the header for sub-type: + // * for latency based samples [0, 8] representing the latency buckets, where 0 is the first one; + // * for error based samples, 0 means all, otherwise the error code; + spanSubtypeQueryField = "zsubtype" + // maxTraceMessageLength is the maximum length of a message in tracez output. + maxTraceMessageLength = 1024 +) + +var ( + defaultLatencies = [...]time.Duration{ + 10 * time.Microsecond, + 100 * time.Microsecond, + time.Millisecond, + 10 * time.Millisecond, + 100 * time.Millisecond, + time.Second, + 10 * time.Second, + 100 * time.Second, + } + canonicalCodes = [...]string{ + "OK", + "CANCELLED", + "UNKNOWN", + "INVALID_ARGUMENT", + "DEADLINE_EXCEEDED", + "NOT_FOUND", + "ALREADY_EXISTS", + "PERMISSION_DENIED", + "RESOURCE_EXHAUSTED", + "FAILED_PRECONDITION", + "ABORTED", + "OUT_OF_RANGE", + "UNIMPLEMENTED", + "INTERNAL", + "UNAVAILABLE", + "DATA_LOSS", + "UNAUTHENTICATED", + } +) + +func canonicalCodeString(code int32) string { + if code < 0 || int(code) >= len(canonicalCodes) { + return "error code " + strconv.FormatInt(int64(code), 10) + } + return canonicalCodes[code] +} + +func tracezHandler(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + w.Header().Set("Content-Type", "text/html; charset=utf-8") + name := r.Form.Get(spanNameQueryField) + t, _ := strconv.Atoi(r.Form.Get(spanTypeQueryField)) + st, _ := strconv.Atoi(r.Form.Get(spanSubtypeQueryField)) + WriteHTMLTracezPage(w, name, t, st) +} + +// WriteHTMLTracezPage writes an HTML document to w containing locally-sampled trace spans. +func WriteHTMLTracezPage(w io.Writer, spanName string, spanType, spanSubtype int) { + if err := headerTemplate.Execute(w, headerData{Title: "Trace Spans"}); err != nil { + log.Printf("zpages: executing template: %v", err) + } + WriteHTMLTracezSummary(w) + WriteHTMLTracezSpans(w, spanName, spanType, spanSubtype) + if err := footerTemplate.Execute(w, nil); err != nil { + log.Printf("zpages: executing template: %v", err) + } +} + +// WriteHTMLTracezSummary writes HTML to w containing a summary of locally-sampled trace spans. +// +// It includes neither a header nor footer, so you can embed this data in other pages. +func WriteHTMLTracezSummary(w io.Writer) { + if err := summaryTableTemplate.Execute(w, getSummaryPageData()); err != nil { + log.Printf("zpages: executing template: %v", err) + } +} + +// WriteHTMLTracezSpans writes HTML to w containing locally-sampled trace spans. +// +// It includes neither a header nor footer, so you can embed this data in other pages. +func WriteHTMLTracezSpans(w io.Writer, spanName string, spanType, spanSubtype int) { + if spanName == "" { + return + } + if err := tracesTableTemplate.Execute(w, traceDataFromSpans(spanName, traceSpans(spanName, spanType, spanSubtype))); err != nil { + log.Printf("zpages: executing template: %v", err) + } +} + +// WriteTextTracezSpans writes formatted text to w containing locally-sampled trace spans. +func WriteTextTracezSpans(w io.Writer, spanName string, spanType, spanSubtype int) { + spans := traceSpans(spanName, spanType, spanSubtype) + data := traceDataFromSpans(spanName, spans) + writeTextTraces(w, data) +} + +// WriteTextTracezSummary writes formatted text to w containing a summary of locally-sampled trace spans. +func WriteTextTracezSummary(w io.Writer) { + w.Write([]byte("Locally sampled spans summary\n\n")) + + data := getSummaryPageData() + if len(data.Rows) == 0 { + return + } + + tw := tabwriter.NewWriter(w, 8, 8, 1, ' ', 0) + + for i, s := range data.Header { + if i != 0 { + tw.Write([]byte("\t")) + } + tw.Write([]byte(s)) + } + tw.Write([]byte("\n")) + + put := func(x int) { + if x == 0 { + tw.Write([]byte(".\t")) + return + } + fmt.Fprintf(tw, "%d\t", x) + } + for _, r := range data.Rows { + tw.Write([]byte(r.Name)) + tw.Write([]byte("\t")) + put(r.Active) + for _, l := range r.Latency { + put(l) + } + put(r.Errors) + tw.Write([]byte("\n")) + } + tw.Flush() +} + +// traceData contains data for the trace data template. +type traceData struct { + Name string + Num int + Rows []traceRow +} + +type traceRow struct { + Fields [3]string + trace.SpanContext + ParentSpanID trace.SpanID +} + +type events []interface{} + +func (e events) Len() int { return len(e) } +func (e events) Less(i, j int) bool { + var ti time.Time + switch x := e[i].(type) { + case *trace.Annotation: + ti = x.Time + case *trace.MessageEvent: + ti = x.Time + } + switch x := e[j].(type) { + case *trace.Annotation: + return ti.Before(x.Time) + case *trace.MessageEvent: + return ti.Before(x.Time) + } + return false +} + +func (e events) Swap(i, j int) { e[i], e[j] = e[j], e[i] } + +func traceRows(s *trace.SpanData) []traceRow { + start := s.StartTime + + lasty, lastm, lastd := start.Date() + wholeTime := func(t time.Time) string { + return t.Format("2006/01/02-15:04:05") + fmt.Sprintf(".%06d", t.Nanosecond()/1000) + } + formatTime := func(t time.Time) string { + y, m, d := t.Date() + if y == lasty && m == lastm && d == lastd { + return t.Format(" 15:04:05") + fmt.Sprintf(".%06d", t.Nanosecond()/1000) + } + lasty, lastm, lastd = y, m, d + return wholeTime(t) + } + + lastTime := start + formatElapsed := func(t time.Time) string { + d := t.Sub(lastTime) + lastTime = t + u := int64(d / 1000) + // There are five cases for duration printing: + // -1234567890s + // -1234.123456 + // .123456 + // 12345.123456 + // 12345678901s + switch { + case u < -9999999999: + return fmt.Sprintf("%11ds", u/1e6) + case u < 0: + sec := u / 1e6 + u -= sec * 1e6 + return fmt.Sprintf("%5d.%06d", sec, -u) + case u < 1e6: + return fmt.Sprintf(" .%6d", u) + case u <= 99999999999: + sec := u / 1e6 + u -= sec * 1e6 + return fmt.Sprintf("%5d.%06d", sec, u) + default: + return fmt.Sprintf("%11ds", u/1e6) + } + } + + firstRow := traceRow{Fields: [3]string{wholeTime(start), "", ""}, SpanContext: s.SpanContext, ParentSpanID: s.ParentSpanID} + if s.EndTime.IsZero() { + firstRow.Fields[1] = " " + } else { + firstRow.Fields[1] = formatElapsed(s.EndTime) + lastTime = start + } + out := []traceRow{firstRow} + + formatAttributes := func(a map[string]interface{}) string { + if len(a) == 0 { + return "" + } + var keys []string + for key := range a { + keys = append(keys, key) + } + sort.Strings(keys) + var s []string + for _, key := range keys { + val := a[key] + switch val.(type) { + case string: + s = append(s, fmt.Sprintf("%s=%q", key, val)) + default: + s = append(s, fmt.Sprintf("%s=%v", key, val)) + } + } + return "Attributes:{" + strings.Join(s, ", ") + "}" + } + + if s.Status != (trace.Status{}) { + msg := fmt.Sprintf("Status{canonicalCode=%s, description=%q}", + canonicalCodeString(s.Status.Code), s.Status.Message) + out = append(out, traceRow{Fields: [3]string{"", "", msg}}) + } + + if len(s.Attributes) != 0 { + out = append(out, traceRow{Fields: [3]string{"", "", formatAttributes(s.Attributes)}}) + } + + var es events + for i := range s.Annotations { + es = append(es, &s.Annotations[i]) + } + for i := range s.MessageEvents { + es = append(es, &s.MessageEvents[i]) + } + sort.Sort(es) + for _, e := range es { + switch e := e.(type) { + case *trace.Annotation: + msg := e.Message + if len(e.Attributes) != 0 { + msg = msg + " " + formatAttributes(e.Attributes) + } + row := traceRow{Fields: [3]string{ + formatTime(e.Time), + formatElapsed(e.Time), + msg, + }} + out = append(out, row) + case *trace.MessageEvent: + row := traceRow{Fields: [3]string{formatTime(e.Time), formatElapsed(e.Time)}} + switch e.EventType { + case trace.MessageEventTypeSent: + row.Fields[2] = fmt.Sprintf("sent message [%d bytes, %d compressed bytes]", e.UncompressedByteSize, e.CompressedByteSize) + case trace.MessageEventTypeRecv: + row.Fields[2] = fmt.Sprintf("received message [%d bytes, %d compressed bytes]", e.UncompressedByteSize, e.CompressedByteSize) + } + out = append(out, row) + } + } + for i := range out { + if len(out[i].Fields[2]) > maxTraceMessageLength { + out[i].Fields[2] = out[i].Fields[2][:maxTraceMessageLength] + } + } + return out +} + +func traceSpans(spanName string, spanType, spanSubtype int) []*trace.SpanData { + internalTrace := internal.Trace.(interface { + ReportActiveSpans(name string) []*trace.SpanData + ReportSpansByError(name string, code int32) []*trace.SpanData + ReportSpansByLatency(name string, minLatency, maxLatency time.Duration) []*trace.SpanData + }) + var spans []*trace.SpanData + switch spanType { + case 0: // active + spans = internalTrace.ReportActiveSpans(spanName) + case 1: // latency + var min, max time.Duration + n := len(defaultLatencies) + if spanSubtype == 0 { + max = defaultLatencies[0] + } else if spanSubtype == n { + min, max = defaultLatencies[spanSubtype-1], (1<<63)-1 + } else if 0 < spanSubtype && spanSubtype < n { + min, max = defaultLatencies[spanSubtype-1], defaultLatencies[spanSubtype] + } + spans = internalTrace.ReportSpansByLatency(spanName, min, max) + case 2: // error + spans = internalTrace.ReportSpansByError(spanName, 0) + } + return spans +} + +func traceDataFromSpans(name string, spans []*trace.SpanData) traceData { + data := traceData{ + Name: name, + Num: len(spans), + } + for _, s := range spans { + data.Rows = append(data.Rows, traceRows(s)...) + } + return data +} + +func writeTextTraces(w io.Writer, data traceData) { + tw := tabwriter.NewWriter(w, 1, 8, 1, ' ', 0) + fmt.Fprint(tw, "When\tElapsed(s)\tType\n") + for _, r := range data.Rows { + tw.Write([]byte(r.Fields[0])) + tw.Write([]byte("\t")) + tw.Write([]byte(r.Fields[1])) + tw.Write([]byte("\t")) + tw.Write([]byte(r.Fields[2])) + if sc := r.SpanContext; sc != (trace.SpanContext{}) { + fmt.Fprintf(tw, "trace_id: %s span_id: %s", sc.TraceID, sc.SpanID) + if r.ParentSpanID != (trace.SpanID{}) { + fmt.Fprintf(tw, " parent_span_id: %s", r.ParentSpanID) + } + } + tw.Write([]byte("\n")) + } + tw.Flush() +} + +type summaryPageData struct { + Header []string + LatencyBucketNames []string + Links bool + TracesEndpoint string + Rows []summaryPageRow +} + +type summaryPageRow struct { + Name string + Active int + Latency []int + Errors int +} + +func getSummaryPageData() summaryPageData { + data := summaryPageData{ + Links: true, + TracesEndpoint: "tracez", + } + internalTrace := internal.Trace.(interface { + ReportSpansPerMethod() map[string]internal.PerMethodSummary + }) + for name, s := range internalTrace.ReportSpansPerMethod() { + if len(data.Header) == 0 { + data.Header = []string{"Name", "Active"} + for _, b := range s.LatencyBuckets { + l := b.MinLatency + s := fmt.Sprintf(">%v", l) + if l == 100*time.Second { + s = ">100s" + } + data.Header = append(data.Header, s) + data.LatencyBucketNames = append(data.LatencyBucketNames, s) + } + data.Header = append(data.Header, "Errors") + } + row := summaryPageRow{Name: name, Active: s.Active} + for _, l := range s.LatencyBuckets { + row.Latency = append(row.Latency, l.Size) + } + for _, e := range s.ErrorBuckets { + row.Errors += e.Size + } + data.Rows = append(data.Rows, row) + } + sort.Slice(data.Rows, func(i, j int) bool { + return data.Rows[i].Name < data.Rows[j].Name + }) + return data +} diff --git a/vendor/go.opencensus.io/zpages/zpages.go b/vendor/go.opencensus.io/zpages/zpages.go new file mode 100644 index 0000000000..5929d1fe77 --- /dev/null +++ b/vendor/go.opencensus.io/zpages/zpages.go @@ -0,0 +1,70 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Package zpages implements a collection of HTML pages that display RPC stats +// and trace data, and also functions to write that same data in plain text to +// an io.Writer. +// +// Users can also embed the HTML for stats and traces in custom status pages. +// +// zpages are currrently work-in-process and cannot display minutely and +// hourly stats correctly. +// +// Performance +// +// Installing the zpages has a performance overhead because additional traces +// and stats will be collected in-process. In most cases, we expect this +// overhead will not be significant but it depends on many factors, including +// how many spans your process creates and how richly annotated they are. +package zpages // import "go.opencensus.io/zpages" + +import ( + "net/http" + "path" + "sync" + + "go.opencensus.io/internal" +) + +// TODO(ramonza): Remove Handler to make initialization lazy. + +// Handler is deprecated: Use Handle. +var Handler http.Handler + +func init() { + mux := http.NewServeMux() + Handle(mux, "/") + Handler = mux +} + +// Handle adds the z-pages to the given ServeMux rooted at pathPrefix. +func Handle(mux *http.ServeMux, pathPrefix string) { + enable() + if mux == nil { + mux = http.DefaultServeMux + } + mux.HandleFunc(path.Join(pathPrefix, "rpcz"), rpczHandler) + mux.HandleFunc(path.Join(pathPrefix, "tracez"), tracezHandler) + mux.Handle(path.Join(pathPrefix, "public/"), http.FileServer(fs)) +} + +var enableOnce sync.Once + +func enable() { + enableOnce.Do(func() { + internal.LocalSpanStoreEnabled = true + registerRPCViews() + }) +} diff --git a/vendor/go.opencensus.io/zpages/zpages_test.go b/vendor/go.opencensus.io/zpages/zpages_test.go new file mode 100644 index 0000000000..c5d8d44ecb --- /dev/null +++ b/vendor/go.opencensus.io/zpages/zpages_test.go @@ -0,0 +1,129 @@ +// Copyright 2017, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package zpages + +import ( + "bytes" + "reflect" + "testing" + "time" + + "fmt" + "net/http" + "net/http/httptest" + + "go.opencensus.io/trace" +) + +var ( + tid = trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 4, 8, 16, 32, 64, 128} + sid = trace.SpanID{1, 2, 4, 8, 16, 32, 64, 128} + sid2 = trace.SpanID{0, 3, 5, 9, 17, 33, 65, 129} +) + +func TestTraceRows(t *testing.T) { + now := time.Now() + later := now.Add(2 * time.Second) + data := traceDataFromSpans("foo", []*trace.SpanData{{ + SpanContext: trace.SpanContext{TraceID: tid, SpanID: sid}, + ParentSpanID: sid2, + Name: "foo", + StartTime: now, + EndTime: later, + Attributes: map[string]interface{}{"stringkey": "stringvalue", "intkey": 42, "boolkey": true}, + Annotations: []trace.Annotation{ + {Time: now.Add(time.Millisecond), Message: "hello, world", Attributes: map[string]interface{}{"foo": "bar"}}, + {Time: now.Add(1500 * time.Millisecond), Message: "hello, world"}, + }, + MessageEvents: []trace.MessageEvent{ + {Time: now, EventType: 2, MessageID: 0x3, UncompressedByteSize: 0x190, CompressedByteSize: 0x12c}, + {Time: later, EventType: 1, MessageID: 0x1, UncompressedByteSize: 0xc8, CompressedByteSize: 0x64}, + }, + Status: trace.Status{ + Code: 1, + Message: "d'oh!", + }, + }}) + fakeTime := "2006/01/02-15:04:05.123456" + for i := range data.Rows { + data.Rows[i].Fields[0] = fakeTime + } + if want := (traceData{ + Name: "foo", + Num: 1, + Rows: []traceRow{ + {Fields: [3]string{fakeTime, " 2.000000", ""}, SpanContext: trace.SpanContext{TraceID: tid, SpanID: sid}, ParentSpanID: sid2}, + {Fields: [3]string{fakeTime, "", `Status{canonicalCode=CANCELLED, description="d'oh!"}`}}, + {Fields: [3]string{fakeTime, "", `Attributes:{boolkey=true, intkey=42, stringkey="stringvalue"}`}}, + {Fields: [3]string{fakeTime, " . 0", "received message [400 bytes, 300 compressed bytes]"}}, + {Fields: [3]string{fakeTime, " . 1000", `hello, world Attributes:{foo="bar"}`}}, + {Fields: [3]string{fakeTime, " 1.499000", "hello, world"}}, + {Fields: [3]string{fakeTime, " .500000", "sent message [200 bytes, 100 compressed bytes]"}}}}); !reflect.DeepEqual(data, want) { + t.Errorf("traceRows: got %v want %v\n", data, want) + } + + var buf bytes.Buffer + writeTextTraces(&buf, data) + if want := `When Elapsed(s) Type +2006/01/02-15:04:05.123456 2.000000 trace_id: 01020304050607080102040810204080 span_id: 0102040810204080 parent_span_id: 0003050911214181 +2006/01/02-15:04:05.123456 Status{canonicalCode=CANCELLED, description="d'oh!"} +2006/01/02-15:04:05.123456 Attributes:{boolkey=true, intkey=42, stringkey="stringvalue"} +2006/01/02-15:04:05.123456 . 0 received message [400 bytes, 300 compressed bytes] +2006/01/02-15:04:05.123456 . 1000 hello, world Attributes:{foo="bar"} +2006/01/02-15:04:05.123456 1.499000 hello, world +2006/01/02-15:04:05.123456 .500000 sent message [200 bytes, 100 compressed bytes] +`; buf.String() != want { + t.Errorf("writeTextTraces: got %q want %q\n", buf.String(), want) + } +} + +func TestGetZPages(t *testing.T) { + mux := http.NewServeMux() + Handle(mux, "/debug") + server := httptest.NewServer(mux) + defer server.Close() + tests := []string{"/debug/rpcz", "/debug/tracez"} + for _, tt := range tests { + t.Run(fmt.Sprintf("GET %s", tt), func(t *testing.T) { + res, err := http.Get(server.URL + tt) + if err != nil { + t.Error(err) + return + } + if got, want := res.StatusCode, http.StatusOK; got != want { + t.Errorf("res.StatusCode = %d; want %d", got, want) + } + }) + } +} + +func TestGetZPages_default(t *testing.T) { + server := httptest.NewServer(Handler) + defer server.Close() + tests := []string{"/rpcz", "/tracez"} + for _, tt := range tests { + t.Run(fmt.Sprintf("GET %s", tt), func(t *testing.T) { + res, err := http.Get(server.URL + tt) + if err != nil { + t.Error(err) + return + } + if got, want := res.StatusCode, http.StatusOK; got != want { + t.Errorf("res.StatusCode = %d; want %d", got, want) + } + }) + } +}