Skip to content

Commit

Permalink
Switch logstream gem to use Cloud API v2 (#22)
Browse files Browse the repository at this point in the history
* Update logstream gem to use Cloud API v2
  • Loading branch information
Zlender authored Mar 25, 2020
1 parent a3f73c0 commit 5230a2b
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 162 deletions.
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM ubuntu:latest

RUN apt-get update \
&& apt-get install -y \
curl \
make \
build-essential \
g++ \
libssl-dev \
ruby-dev
RUN mkdir /src

COPY . /src

RUN cd /src \
&& gem build logstream \
&& gem install logstream-*.gem

ENTRYPOINT ["/bin/bash"]
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2014 Acquia, Inc.
Copyright (c) 2020 Acquia, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
28 changes: 13 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,24 @@ UI</a> as well.

## Quick start

* Logstream works in conjunction with Acquia's <a
href="http://cloudapi.acquia.com/">Cloud API</a>. If you haven't already,
install your <a href="https://accounts.acquia.com/account/security">Acquia
Cloud Drush integration</a> files, which includes your Cloud API credentials.
* Logstream works in conjunction with Acquia's [Cloud API](https://cloudapi-docs.acquia.com/). In order to use Cloud API you will need to generate credentials for your account. [Cloud API v2 authentication](https://docs.acquia.com/acquia-cloud/develop/api/auth/#generating-an-api-token) explains how to generate the credentials. Place the credentials in `$HOME/.acquia/cloudapiv2.conf` . This file is in JSON format with api_key and api_secret defined.
```
{
"api_key" : "key",
"api_secret" : "secret"
}
```

* Install the Logstream CLI:
```
$ gem install logstream
```

* List all the sites you have access to:
```
$ drush ac-site-list
devcloud:mysite
```
* Find the UUID of the application you would like to stream logs for. Documentation on how to find applicaiton UUID can be found [here](https://docs.acquia.com/acquia-cloud/manage/applications/#obtaining-your-subscription-s-application-id)

* Stream logs from the production environment:
```
$ logstream tail devcloud:mysite prod
$ logstream tail 55ea1945-4aa6-4c56-bb7b-2108565e22d6 prod
127.0.0.1 - - [11/Jun/2014:17:28:47 +0000] "GET / HTTP/1.1" 200 7708 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36" http_host=mysite.com affinity="-" upstream_addr="10.218.29.150:80" hosting_site=mysite request_time=0.030 forwarded_for="64.80.128.4" upstream_status="200"
... etc ...
```
Expand All @@ -44,11 +43,10 @@ A variety of filtering and display options are available:
$ logstream help tail
```

## API v1
## API v2

Logstream communicates over TCP using the WebSocket protocol. Use the <a
href="http://cloudapi.acquia.com/#GET__sites__site_envs__env_logstream-instance_route">logstream
Cloud API call</a> to retrieve the URL to connect to and an authenticated
Logstream communicates over TCP using the WebSocket protocol. Use the [logstream
Cloud API call](https://cloudapi-docs.acquia.com/#/Environments/getEnvironmentsLogstream) to retrieve the URL to connect to and an authenticated
message to initial streaming for a particular Cloud environment.

Messages use text data frames, and contain JSON-encoded hashes. Each message
Expand All @@ -73,7 +71,7 @@ default, the CLI enables the log types apache-request, php-error,
drupal-watchdog, and varnish-request.

```
$ logstream tail devcloud:mysite dev --debug
$ logstream tail 55ea1945-4aa6-4c56-bb7b-2108565e22d6 dev --debug
-> connect to wss://logstream.acquia.com/ah_websocket/logstream/v1
-> {"site":"devcloud:mysite","d":"deaefc1f42a4d18cb932c2eb9fa75115fba5ab83f1a3c564767ef1ce8dabf2cc","t":1404764927,"env":"dev","cmd":"stream-environment"}
<- {"cmd":"connected","server":"logstream-api-61"}
Expand Down
65 changes: 24 additions & 41 deletions bin/logstream
Original file line number Diff line number Diff line change
@@ -1,45 +1,13 @@
#!/usr/bin/env ruby

require 'rubygems'
require 'net/https'
require 'json'
require 'thor'
require 'json'
require 'logstream/client'

# @todo: Yeah, this is terrible. Replace it with a real Cloud API gem.
class QuickCloudAPI
class Error < StandardError; end

def self.get(path, opts = {})
confpath = "#{ENV['HOME']}/.acquia/cloudapi.conf"
begin
json = File.read(confpath)
config = JSON.load(json)
rescue Errno::ENOENT, JSON::ParserError => e
raise Error, "#{confpath} is missing or invalid. Download your Drush aliases from https://accounts.acquia.com/account/security to initialize it."
end
opts[:endpoint] ||= "https://cloudapi.acquia.com/v1"
uri = URI.parse("#{opts[:endpoint]}#{path}.json")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.ca_file = File.dirname(__FILE__) + "/../etc/ca.pem"
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
request = Net::HTTP::Get.new(uri.request_uri)
request.basic_auth(config['email'], config['key'])
response = http.request(request)
parsed = JSON.parse(response.body) rescue nil
case response.code.to_i
when 200
raise Error, "Unexpected reply #{response.body}" unless parsed
parsed
else
raise Error, "HTTP #{response.code}: #{response.body}"
end
end
end
require 'logstream/cloudapi_v2'

class LogTailorCLI < Thor
desc "tail SITE ENV", "Stream log information for the specified site environment."
desc "tail APPLICATION_UUID ENV", "Stream log information for the specified application environment."
method_option(:types, :type => :array, :aliases => '-t',
:desc => "Only display listed log types",
:default => %w(apache-request php-error drupal-watchdog varnish-request),
Expand All @@ -59,15 +27,30 @@ class LogTailorCLI < Thor
:banner => "disp_time http_status log_type request_id server text"
)
method_option(:color, :type => :boolean, :default => true, :desc => 'Turn on or off colorized lines.')
method_option(:endpoint, :type => :string, :desc => 'The Cloud API URL to connect to.')
method_option(:debug, :type => :boolean, :default => false, :desc => 'Turn on debugging.')
def tail(site, env)
def tail(application_uuid, env)
begin
shows = Hash[options[:show].map { |s| s.split('=') }.map { |k,v| [k, Regexp.new(v)] }] rescue {}
hides = Hash[options[:hide].map { |h| h.split('=') }.map { |k,v| [k, Regexp.new(v)] }] rescue {}

begin
info = QuickCloudAPI.get("/sites/#{site}/envs/#{env}/logstream", { :endpoint => options[:endpoint] })
confpath = "#{ENV['HOME']}/.acquia/cloudapiv2.conf"
begin
json = File.read(confpath)
config = JSON.load(json)
rescue Errno::ENOENT, JSON::ParserError => e
raise Error, "#{confpath} is missing or invalid. Please configure your Cloud API v2 credentials"
end
cloudapi_client = Logstream::CloudAPIV2.new(config['api_key'], config['api_secret'])
environments = cloudapi_client.get_application_environments(application_uuid)
environment_uuid = nil
environments.each do |environment|
if environment['name'] == env
environment_uuid = environment['id']
end
end
raise Error, "No environment found with #{env} name." unless environment_uuid
stream_info = cloudapi_client.get_envirornment_logstream(environment_uuid)
logstream = Logstream::Client.new({
:columns => options[:columns],
:types => options[:types],
Expand All @@ -76,14 +59,14 @@ class LogTailorCLI < Thor
:no_color => !options[:color],
:debug => options[:debug],
})
logstream.run(info['url'], info['msg'])
rescue QuickCloudAPI::Error => e
logstream.run(stream_info['url'], stream_info['params'])
rescue Logstream::CloudAPIV2::Error => e
puts "Cloud API error: #{e.message}"
exit(1)
end
end
end
end

LogTailorCLI.start
LogTailorCLI.start(ARGV)

98 changes: 0 additions & 98 deletions etc/ca.pem

This file was deleted.

1 change: 1 addition & 0 deletions lib/logstream.rb
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
require 'logstream/client'
require 'logstream/cloudapi_v2'
16 changes: 13 additions & 3 deletions lib/logstream/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,28 @@ def debug_recv(msg)
debug("<- #{msg}")
end

def run(url, connect_message)
def run(url, info)
EM.run do
debug_send("connect to #{url}")
connect_message = {
'cmd' => 'stream-environment',
'site' => info['site'],
'env' => info['environment'],
't' => info['t'],
'd' => info['hmac'],
}
ws = Faye::WebSocket::Client.new(url)
ws.on :open do
debug_send(connect_message)
ws.send(connect_message)
@running = false
end
ws.on :message do |body,type|
debug_recv(body.data)
msg = JSON.parse(body.data)
case msg['cmd']
when 'connected'
debug_send(connect_message.to_json)
ws.send(connect_message.to_json) unless @running
@running = true
when 'success'
color('logtailor-error', msg['code']) do
# puts "#{msg.inspect}"
Expand Down
60 changes: 60 additions & 0 deletions lib/logstream/cloudapi_v2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
require 'net/https'
require 'json'

module Logstream
class CloudAPIV2
class Error < StandardError; end

attr_accessor :client_id, :client_secret, :endpoint
CLOUDAPI_ENDPOINT = 'https://cloud.acquia.com/api'

def initialize(client_id, client_secret)
@client_id = client_id
@client_secret = client_secret
end

def get(path)
bearer_token = get_token
uri = URI.parse("#{CLOUDAPI_ENDPOINT}#{path}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri.request_uri)
request['Authorization'] = "Bearer #{bearer_token}"
response = http.request(request)
parsed = JSON.parse(response.body) rescue nil
case response.code.to_i
when 200
raise Error, "Unexpected reply #{response.body}" unless parsed
parsed
else
raise Error, "HTTP #{response.code}: #{response.body}"
end
end

def get_application_environments(application_uuid)
response = get("/applications/#{application_uuid}/environments") #, { :query => { "filter" => "name%3D#{env}"}})
raise Error, "No Environments found." if response['total'] == 0
raise Error, "Unexpected reply #{response}" unless response['_embedded']['items']
response['_embedded']['items']
end

def get_envirornment_logstream(environment_uuid)
response = get("/environments/#{environment_uuid}/logstream")
raise Error, "Unexpected reply #{response}" unless response['logstream']
response['logstream']
end

def get_token
uri = URI.parse("https://accounts.acquia.com/api/auth/oauth/token")
response = Net::HTTP.post_form(uri, 'client_id' => @client_id, 'client_secret' => @client_secret, 'grant_type' => 'client_credentials')
parsed = JSON.parse(response.body) rescue nil
case response.code.to_i
when 200
raise Error, "Unexpected reply #{response.body}" unless parsed["access_token"]
parsed["access_token"]
else
raise Error, "HTTP #{response.code}: #{response.body}"
end
end
end
end
Loading

0 comments on commit 5230a2b

Please sign in to comment.