/bc-lightstep-ruby

Generic lightstep library for distributed tracing in ruby

Primary LanguageRubyMIT LicenseMIT

bc-lightstep-ruby - LightStep distributed tracing

CircleCI Gem Version Inline docs Maintainability Test Coverage

Adds LightStep tracing support for Ruby. This is an extension of the LightStep ruby gem and adds extra functionality and resiliency.

Installation

gem 'bc-lightstep-ruby'

Then in an initializer or before use:

require 'bigcommerce/lightstep'
Bigcommerce::Lightstep.configure do |c|
  c.component_name = 'myapp'
  c.access_token = 'abcdefg'
  c.host = 'my.lightstep.service.io'
  c.port = 8080
  c.verbosity = 1
end

Then in your script:

tracer = Bigcommerce::Lightstep::Tracer.instance
tracer.start_span('my-span', context: request.headers) do |span|
  span.set_tag('my-tag', 'value')
  # do thing to measure
end

Environment Config

bc-lightstep-ruby can be automatically configured from these ENV vars, if you'd rather use that instead:

Name Description
LIGHTSTEP_ENABLED Flag to determine whether to broadcast spans. Defaults to (1) enabled, 0 will disable.
LIGHTSTEP_COMPONENT_NAME The component name to use
LIGHTSTEP_ACCESS_TOKEN The access token to use to connect to the collector. Optional.
LIGHTSTEP_HOST Host of the collector.
LIGHTSTEP_PORT Port of the collector.
LIGHTSTEP_HTTP1_ERROR_CODE The HTTP error code to report in spans for internal errors
LIGHTSTEP_HTTP1_ERROR_CODE_MINIMUM The minimum HTTP error code value to be considered an error for span tag purposes.
LIGHTSTEP_CONTROLLER_PREFIX The prefix for Rails controllers to use
LIGHTSTEP_SSL_VERIFY_PEER If using 443 as the port, toggle SSL verification.
LIGHTSTEP_MAX_BUFFERED_SPANS The maximum number of spans to buffer before dropping.
LIGHTSTEP_MAX_LOG_RECORDS Maximum number of log records on a span to accept.
LIGHTSTEP_MAX_REPORTING_INTERVAL_SECONDS The maximum number of seconds to wait before flushing the report to the collector.
LIGHTSTEP_ACTIVE_RECORD_ENABLED Whether or not to add ActiveRecord mysql spans. Only works with mysql2 gem.
LIGHTSTEP_ACTIVE_RECORD_ALLOW_AS_ROOT_SPAN Allow ActiveRecord mysql spans to be the root span?
LIGHTSTEP_ACTIVE_RECORD_SPAN_PREFIX What to prefix the ActiveRecord mysql span with
LIGHTSTEP_REDIS_ALLOW_AS_ROOT_SPAN Allow redis to be the root span?
LIGHTSTEP_REDIS_EXCLUDED_COMMANDS Redis commands to exclude from spans. Comma-separated list.
LIGHTSTEP_VERBOSITY The verbosity level of lightstep logs.

Most systems will only need to customize the component name.

Instrumenting Rails Controllers

Just drop this include into ApplicationController:

include Bigcommerce::Lightstep::RailsControllerInstrumentation

Faraday Middleware

To use the supplied faraday middleware, simply:

Faraday.new do |faraday|
  faraday.use Bigcommerce::Lightstep::Middleware::Faraday, 'name-of-external-service'
end

Spans will be built with the external service name. It's generally not a good idea to use the Faraday adapter with internal services that are also instrumented with LightStep - use the Faraday adapter on external services or systems outside of your instrumenting control.

Redis

This gem will automatically detect and instrument Redis calls when they are made using the Redis::Client class. It will set as tags on the span the host, port, db instance, and the command (but no arguments).

Note that this will not record redis timings if they are a root span. This is to prevent trace spamming. You can re-enable this by setting the redis_allow_root_spans configuration option to true.

It also excludes ping commands, and you can provide a custom list by setting the redis_excluded_commands configuration option to an array of commands to exclude.

ActiveRecord and MySQL

This gem will automatically instrument MySQL queries with spans when made with the mysql2 gem and ActiveRecord. It will set as tags on the span the host, database type, database name, and a sanitized version of the SQL query made. The query will have no values - replaced with ? - to ensure secure logging and no leaking of PII data.

Note that this will not record mysql timings if they are a root span. This is to prevent trace spamming. You can configure this gem to allow it via ENV, but it is not recommended.

By default, it will also exclude COMMIT, SCHEMA, and SHOW FULL FIELDS queries.

Individual methods

You can easily instrument individual methods with the Traceable module and trace method:

class MyService
  include ::Bigcommerce::Lightstep::Traceable

  trace :call, 'operation.do-my-thing' do |span:, product:, options:|
    span.set_tag('product_id', product.id)
  end
  # or, with no block:
  trace :call, 'operation.do-my-thing'

  def call(product:, options:)
    # ...
  end
end

Tracing Positional Argument Methods

For positional argument methods, the behavior is a bit different. In your trace call, if tracing a method with positional arguments, you'll need to have the block arguments be positional as well:

class MyService
  include ::Bigcommerce::Lightstep::Traceable

  trace :positional, 'operation.do-my-thing' do |span, product, options|
    span.set_tag('product_id', product.id)
  end
  def positional(product, options = {})
    # ...
  end
end

Note that any default values in the argument will not carry over into the trace block. Secondly, with positional argument methods that have only a single hash argument, since this library has no way to detect in that case if it is keyword-arguments or a single hash argument, the library will simply add the span to the hash itself, and you'll need to adjust the trace block accordingly:

class MyService
  include ::Bigcommerce::Lightstep::Traceable

  trace :positional_single_hash_arg, 'operation.do-my-thing' do |my_hash|
    my_hash[:span].set_tag('product_id', my_hash[:product_id])
  end
  def positional_single_hash_arg(my_hash)
    # ...
  end
end

It is recommended for this reason - and others - to never use single-hash positional arguments in Ruby.

RSpec

This library comes with a built-in matcher for testing span blocks. In your rspec config:

require 'bigcommerce/lightstep/rspec'

Then, in a test:

it 'should create a lightstep span' do
  expect { my_code_here }.to create_a_lightstep_span(name: 'my-span-name', tags: { tag_one: 'value-here' })
end

Global Interceptors

This library has global interceptor support that will allow access to each span as it is built. This allows you to dynamically inject tags or alter spans as they are collected. You can configure interceptors via an initializer:

Bigcommerce::Lightstep.configure do |c|
  c.interceptors.use(MyInterceptor, an_option: 123)
  # or, alternatively:
  c.interceptors.use(MyInterceptor.new(an_option: 123))
end

It's important to note that this is a CPU-intensive operation as interceptors will be run for every start_span tag, so don't build interceptors that require lots of processing power or that would impact latencies.

ENV Interceptor

Provided out of the box is an interceptor to automatically inject ENV vars into span tags. You can configure like so:

Bigcommerce::Lightstep.configure do |c|
  c.interceptors.use(::Bigcommerce::Lightstep::Interceptors::Env.new(
    keys: {
      version: 'VERSION'
    },
    presets: [:nomad, :hostname]
  ))
end

The keys argument allows you to pass a span tag => ENV key mapping that will assign those ENV vars to spans. The presets argument comes with a bunch of preset mappings you can use rather than manually mapping them yourself.

Note that this interceptor must be instantiated in configuration, rather than passing the class and options, as it needs to pre-materialize the ENV values to reduce CPU usage.

License

Copyright (c) 2018-present, BigCommerce Pty. Ltd. All rights reserved

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.