Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SHACK-188] telemetry cleanup tasks #134

Merged
merged 6 commits into from
May 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions components/chef-cli/lib/chef-cli/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,22 +79,34 @@ def setup_cli
# Creates the tree we need under ~/.chef-workstation
# based on config settings:
Config.create_directory_tree
# TODO because we have not loaded a command, we wil lalways be using

# TODO because we have not loaded a command, we will always be using
# the default location at this step.
if Config.using_default_location? && !Config.exist?
UI::Terminal.output T.creating_config(Config.default_location)
Config.create_default_config_file
# Tell the user we're anonymously tracking, give brief opt-out
# and a link to detailed information.
UI::Terminal.output ""
UI::Terminal.output T.telemetry_enabled(Config.default_location)
UI::Terminal.output ""
setup_workstation
end

Config.load
ChefCLI::Log.setup(Config.log.location, Config.log.level.to_sym)
ChefCLI::Log.info("Initialized logger")
end

# This setup command is run if ".chef-workstation" is missing prior to
# the run. It will set up default configuration, generated an installation id
# for telemetry, and report telemetry & config info to the operator.
def setup_workstation
require "securerandom"
installation_id = SecureRandom.uuid
File.write(Config.telemetry_installation_identifier_file, installation_id)
UI::Terminal.output T.creating_config(Config.default_location)
Config.create_default_config_file
# Tell the user we're anonymously tracking, give brief opt-out
# and a link to detailed information.
UI::Terminal.output ""
UI::Terminal.output T.telemetry_enabled(Config.default_location)
UI::Terminal.output ""
end

def perform_command
update_args_for_help
update_args_for_version
Expand Down
4 changes: 4 additions & 0 deletions components/chef-cli/lib/chef-cli/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ def telemetry_session_file
File.join(telemetry_path, "TELEMETRY_SESSION_ID")
end

def telemetry_installation_identifier_file
File.join(WS_BASE_PATH, "installation_id")
end

def error_output_path
File.join(File.dirname(log.location), "errors.txt")
end
Expand Down
31 changes: 22 additions & 9 deletions components/chef-cli/lib/chef-cli/telemeter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ module ChefCLI
# a main 'timed_capture', and it would be good to see ordering within nested calls.
class Telemeter
include Singleton
DEFAULT_INSTALLATION_GUID = "00000000-0000-0000-0000-000000000000"

class << self
extend Forwardable
def_delegators :instance, :timed_capture, :capture, :commit, :timed_action_capture, :timed_run_capture
Expand Down Expand Up @@ -93,15 +95,25 @@ def commit
end

def make_event_payload(name, data)
properties = {
# We will submit this payload in a future run, so capture the time of actual execution:
run_timestamp: run_timestamp,
# This lets us filter out testing/dev actions, which may not
# follow customer usage patterns:
telemetry_mode: ChefCLI::Config.telemetry.dev ? "dev" : "prod",
host_platform: host_platform,
{
event: name,
properties: {
installation_id: installation_id,
run_timestamp: run_timestamp,
host_platform: host_platform,
event_data: data
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

}
{ event: name, properties: properties.merge(data) }
end

def installation_id
@installation_id ||=
begin
File.read(ChefCLI::Config.telemetry_installation_identifier_file).chomp
rescue
ChefCLI::Log.info "could not read #{ChefCLI::Config.telemetry_installation_identifier_file} - using default id"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this sort of user output something that should be defined in the i18n data?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, just a log entry. The user need not know or care about this issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OOOOOH, yea. LOG. Right.

DEFAULT_INSTALLATION_GUID
end
end

# For testing.
Expand All @@ -125,7 +137,8 @@ def host_platform
end

def convert_events_to_session
YAML.dump({ "entries" => @events_to_send })
YAML.dump({ "version" => ChefCLI::VERSION,
"entries" => @events_to_send })
end

def write_session(session)
Expand Down
22 changes: 19 additions & 3 deletions components/chef-cli/lib/chef-cli/telemeter/sender.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,20 @@ module ChefCLI
class Telemeter
class Sender
def run
session_files.each { |path| process_session(path) }
if ChefCLI::Telemeter.enabled?
# dev mode telemetry gets sent to a different location
if ChefCLI::Config.telemetry.dev
ENV["CHEF_TELEMETRY_ENDPOINT"] ||= "https://telemetry-acceptance.chef.io"
end
session_files.each { |path| process_session(path) }
else
# If telemetry is not enabled, just clean up and return. Even though
# the telemetry gem will not send if disabled, log output saying that we're submitting
# it when it has been disabled can be alarming.
ChefCLI::Log.info("Telemetry disabled, clearing any existing session captures without sending them.")
session_files.each { |path| FileUtils.rm_rf(path) }
end
FileUtils.rm_rf(ChefCLI::Config.telemetry_session_file)
ChefCLI::Log.info("Terminating, nothing more to do.")
end

Expand All @@ -30,11 +43,14 @@ def submit_session(content)
# Each run is one session, so we'll first remove remove the session file
# to force creating a new one.
FileUtils.rm_rf(ChefCLI::Config.telemetry_session_file)
# We'll use the version captured in the sesion file
entries = content["entries"]
cli_version = content["version"]
total = entries.length
telemetry = Telemetry.new(product: "chef-workstation-cli",
origin: "command-line",
product_version: ChefCLI::VERSION,
product_version: cli_version,
install_context: "omnibus")
entries = content["entries"]
total = entries.length
entries.each_with_index do |entry, x|
submit_entry(telemetry, entry, x + 1, total)
Expand Down
65 changes: 57 additions & 8 deletions components/chef-cli/spec/unit/telemeter/sender_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,64 @@

RSpec.describe ChefCLI::Telemeter::Sender do
let(:subject) { ChefCLI::Telemeter::Sender.new }
let(:enabled_flag) { true }
let(:dev_mode) { false }
let(:config) { double("config") }

before do
allow(config).to receive(:dev).and_return dev_mode
allow(ChefCLI::Config).to receive(:telemetry).and_return config
allow(ChefCLI::Telemeter).to receive(:enabled?).and_return enabled_flag
# Ensure this is not set for each test:
ENV.delete("CHEF_TELEMETRY_ENDPOINT")
end

describe "#run" do
let(:session_files) { %w{file1 file2} }
it "submits the session capture for each session file found" do
before do
expect(subject).to receive(:session_files).and_return session_files
expect(subject).to receive(:process_session).with("file1")
expect(subject).to receive(:process_session).with("file2")
subject.run
end

context "when telemetry is disabled" do
let(:enabled_flag) { false }
it "deletes session files without sending" do
expect(FileUtils).to receive(:rm_rf).with("file1")
expect(FileUtils).to receive(:rm_rf).with("file2")
expect(FileUtils).to receive(:rm_rf).with(ChefCLI::Config.telemetry_session_file)
expect(subject).to_not receive(:process_session)
subject.run
end
end

context "when telemetry is enabled" do
context "and telemetry dev mode is true" do
let(:dev_mode) { true }
let(:session_files) { [] } # Ensure we don't send anything without mocking :allthecalls:
context "and a custom telemetry endpoint is not set" do
it "configures the environment to submit to the Acceptance telemetry endpoint" do
subject.run
expect(ENV["CHEF_TELEMETRY_ENDPOINT"]).to eq "https://telemetry-acceptance.chef.io"
end
end

context "and a custom telemetry endpoint is already set" do
before do
ENV["CHEF_TELEMETRY_ENDPOINT"] = "https://localhost"
end
it "should not overwrite the custom value" do
subject.run
expect(ENV["CHEF_TELEMETRY_ENDPOINT"]).to eq "https://localhost"
end
end
end

it "submits the session capture for each session file found" do
expect(subject).to receive(:process_session).with("file1")
expect(subject).to receive(:process_session).with("file2")
expect(FileUtils).to receive(:rm_rf).with(ChefCLI::Config.telemetry_session_file)
subject.run
end
end
end

describe "#session_files" do
Expand All @@ -41,8 +90,8 @@

describe "process_session" do
it "loads the sesion and submits it" do
expect(subject).to receive(:load_and_clear_session).with("file1").and_return({ "entries" => [] })
expect(subject).to receive(:submit_session).with({ "entries" => [] })
expect(subject).to receive(:load_and_clear_session).with("file1").and_return({ "version" => "1.0.0", "entries" => [] })
expect(subject).to receive(:submit_session).with({ "version" => "1.0.0", "entries" => [] })
subject.process_session("file1")
end
end
Expand All @@ -55,7 +104,8 @@
expect(Telemetry).to receive(:new).and_return telemetry
expect(subject).to receive(:submit_entry).with(telemetry, { "event" => "action1" }, 1, 2)
expect(subject).to receive(:submit_entry).with(telemetry, { "event" => "action2" }, 2, 2)
subject.submit_session( { "entries" => [ { "event" => "action1" }, { "event" => "action2" } ] } )
subject.submit_session( { "version" => "1.0.0",
"entries" => [ { "event" => "action1" }, { "event" => "action2" } ] } )
end
end

Expand All @@ -66,5 +116,4 @@
subject.submit_entry(telemetry, { "test" => "this" }, 1, 1)
end
end

end
61 changes: 47 additions & 14 deletions components/chef-cli/spec/unit/telemeter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,9 @@

RSpec.describe ChefCLI::Telemeter do
subject { ChefCLI::Telemeter.instance }
let(:dev_mode) { true }
let(:config) { double("config") }
let(:host_platform) { "linux" }

before do
allow(config).to receive(:dev).and_return dev_mode
allow(ChefCLI::Config).to receive(:telemetry).and_return config
allow(subject).to receive(:host_platform).and_return host_platform
end

Expand Down Expand Up @@ -103,6 +99,38 @@
end
end

context "::enabled?" do
let(:enabled_flag) { false }
let(:config) { double("config") }
before do
allow(ChefCLI::Config).to receive(:telemetry).and_return(config)
allow(config).to receive(:enable).and_return(enabled_flag)
end

context "when config value is enabled" do
let(:enabled_flag) { true }
context "and CHEF_TELEMETRY_OPT_OUT is not present in env vars" do
it "returns false" do
ENV.delete("CHEF_TELEMETRY_OPT_OUT")
expect(subject.enabled?).to eq true
end
end
context "and CHEF_TELEMETRY_OPT_OUT is present in env vars" do
it "returns false" do
ENV["CHEF_TELEMETRY_OPT_OUT"] = ""
expect(subject.enabled?).to eq false
end
end
end

context "when config value is disabled" do
let(:enabled_flag) { false }
it "returns false" do
expect(subject.enabled?).to eq false
end
end
end

context "#timed_run_capture" do
it "invokes timed_capture with run data" do
expected_data = { arguments: [ "arg1" ] }
Expand Down Expand Up @@ -141,17 +169,22 @@
end

context "#make_event_payload" do
context "when event is ':run'" do
it "adds expected properties" do
payload = subject.make_event_payload(:run, { hello: "world" })
expect(payload[:event]).to eq :run
props = payload[:properties]
expect(props[:telemetry_mode]).to eq "dev"
expect(props[:host_platform]).to eq host_platform
expect(props[:run_timestamp]).to_not eq nil
expect(props[:hello]).to eq "world"
end
before do
allow(subject).to receive(:installation_id).and_return "0000"
end

it "adds expected properties" do
payload = subject.make_event_payload(:run, { hello: "world" })
expected_payload = {
event: :run,
properties: {
installation_id: "0000",
run_timestamp: subject.run_timestamp,
host_platform: host_platform,
event_data: { hello: "world" }
}
}
expect(payload).to eq expected_payload
end
end
end