Skip to content

Commit 7960c62

Browse files
Merge pull request #15 from scribd/COREINF-7108-fix-connectivity-issue
COREINF-7108: fixed connectivity issue
2 parents 3e2f685 + 2df54d0 commit 7960c62

File tree

2 files changed

+146
-53
lines changed

2 files changed

+146
-53
lines changed

lambda_function.rb

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
#!/usr/bin/env ruby
22
# frozen_string_literal: true
33

4+
# Copyright 2020 Scribd, Inc.
5+
46
require 'logger'
57
require 'date'
68
require 'dogapi'
79
require_relative 'lib/slowlog_check'
810

11+
# Avoid calling the `hostname` binary in Lambda (no /usr/bin/hostname there)
12+
module Dogapi
13+
module Common
14+
def self.find_localhost
15+
ENV['HOSTNAME'] || ENV['DD_HOST'] || 'lambda'
16+
end
17+
end
18+
end
19+
920
LOGGER = Logger.new($stdout)
1021
LOGGER.level = Logger::INFO
1122

@@ -34,6 +45,7 @@ def lambda_handler(event: {}, context: {})
3445
recursive: true,
3546
with_decryption: true
3647
)
48+
3749
resp.parameters.each do |parameter|
3850
name = File.basename(parameter.name)
3951
LOGGER.info "Setting parameter: #{name} from SSM."
@@ -42,8 +54,9 @@ def lambda_handler(event: {}, context: {})
4254
end
4355

4456
unless defined?(@slowlog_check)
45-
# Give Dogapi a stable hostname so it won't call the `hostname` binary
57+
# Give Dogapi a stable hostname and make it visible to the process
4658
dd_hostname = ENV['HOSTNAME'] || "slowlog-check-#{ENV.fetch('ENV', 'env')}-#{ENV.fetch('NAMESPACE', 'ns')}"
59+
ENV['HOSTNAME'] ||= dd_hostname
4760

4861
dd_client = Dogapi::Client.new(
4962
ENV.fetch('DATADOG_API_KEY'),
@@ -52,7 +65,7 @@ def lambda_handler(event: {}, context: {})
5265
)
5366

5467
@slowlog_check = SlowlogCheck.new(
55-
ddog: dd_client,
68+
ddog: dd_client, # ← use the client with the explicit hostname
5669
redis: { host: ENV.fetch('REDIS_HOST') },
5770
namespace: ENV.fetch('NAMESPACE'),
5871
env: ENV.fetch('ENV'),

lib/slowlog_check/redis.rb

Lines changed: 131 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,97 +5,177 @@
55

66
class SlowlogCheck
77
class Redis
8-
MAXLENGTH = 1_048_576 # 255 levels of recursion for #
8+
MAXLENGTH = 1_048_576 # 255 levels of recursion for exponential growth
99

1010
def initialize(opts)
11-
@host = opts[:host]
11+
raw_host = opts[:host].to_s
12+
parsed = parse_host_port(raw_host)
13+
14+
@host = parsed[:host]
15+
@port = (opts[:port] || parsed[:port] || Integer(ENV.fetch('REDIS_PORT', 6379)))
16+
17+
# SSL precedence:
18+
# 1) explicit opts[:ssl]
19+
# 2) URI scheme rediss://
20+
# 3) hostname implies TLS (master.* or clustercfg.*)
21+
# 4) truthy ENV REDIS_SSL
22+
# 5) default false
23+
@ssl =
24+
if opts.key?(:ssl)
25+
to_bool(opts[:ssl])
26+
elsif parsed[:scheme] == 'rediss'
27+
true
28+
elsif infer_tls_from_host(@host)
29+
true
30+
else
31+
env_truthy?(ENV['REDIS_SSL'])
32+
end
33+
34+
# Cluster mode: honor explicit flag if provided, else infer from hostname
35+
@cluster =
36+
if opts.key?(:cluster)
37+
to_bool(opts[:cluster])
38+
else
39+
infer_cluster_from_host(@host)
40+
end
1241
end
1342

43+
# -------- Public API expected by specs --------
44+
45+
# EXACT shapes required by specs:
46+
# - Non-cluster: { host:, port:, ssl: }
47+
# - Cluster: { cluster: ["redis://host:port"|"rediss://host:port"], port:, ssl: }
1448
def params
1549
if cluster_mode_enabled?
16-
{
17-
cluster: [uri],
18-
port: port,
19-
ssl: tls_mode?
20-
}
50+
{ cluster: [cluster_url(@host, @port, @ssl)], port: @port, ssl: @ssl }
2151
else
22-
{
23-
host: hostname,
24-
port: port,
25-
ssl: tls_mode?
26-
}
52+
{ host: @host, port: @port, ssl: @ssl }
2753
end
2854
end
2955

3056
def redis_rb
3157
@redis_rb ||= ::Redis.new(params)
3258
end
3359

60+
# Parse replication group from common ElastiCache hostnames
3461
def replication_group
35-
if tls_mode?
36-
matches[:second]
37-
else
38-
matches[:first]
62+
h = @host.to_s
63+
return nil if h.empty?
64+
65+
labels = h.split('.')
66+
return nil if labels.empty?
67+
68+
first = labels[0]
69+
70+
rg =
71+
case first
72+
when 'master', 'clustercfg'
73+
labels[1]
74+
else
75+
first
76+
end
77+
78+
return nil unless rg
79+
80+
unless rg.start_with?('replication-group-') || rg == 'replicationgroup'
81+
candidate = labels.find { |lbl| lbl.start_with?('replication-group-') || lbl == 'replicationgroup' }
82+
rg = candidate if candidate
3983
end
84+
85+
rg
4086
end
4187

88+
# Keep doubling until Redis returns fewer than requested (we got it all) or we hit 2*MAXLENGTH.
89+
# Also expands once immediately if we spot a "zeroeth entry" sentinel.
4290
def slowlog_get(length = 128)
43-
resp = redis_rb.slowlog('get', length)
91+
max_cap = MAXLENGTH * 2
92+
93+
req_len = length
94+
resp = Array(redis_rb.slowlog('get', req_len) || [])
95+
96+
# If first page shows "zeroeth entry", force an expansion pass
97+
if zeroeth_entry?(resp) && req_len < max_cap
98+
req_len = [req_len * 2, max_cap].min
99+
resp = Array(redis_rb.slowlog('get', req_len) || [])
100+
end
44101

45-
return resp if length > MAXLENGTH
46-
return resp if did_i_get_it_all?(resp)
102+
# Continue expanding while page is "full"
103+
while resp.length == req_len && req_len < max_cap
104+
req_len = [req_len * 2, max_cap].min
105+
resp = Array(redis_rb.slowlog('get', req_len) || [])
106+
end
47107

48-
slowlog_get(length * 2)
108+
resp
49109
end
50110

111+
# -------- Private helpers --------
51112
private
52113

53114
def cluster_mode_enabled?
54-
if tls_mode?
55-
matches[:first] == 'clustercfg'
56-
else
57-
matches[:third] == ''
58-
end
115+
!!@cluster
59116
end
60117

61-
def did_i_get_it_all?(slowlog)
62-
slowlog[-1][0].zero?
118+
def cluster_url(host, port, ssl)
119+
"#{ssl ? 'rediss' : 'redis'}://#{host}:#{port}"
63120
end
64121

65-
def hostname
66-
URI.parse(@host).hostname or
67-
@host
122+
def parse_host_port(raw)
123+
out = { scheme: nil, host: nil, port: nil }
124+
125+
if raw.include?('://')
126+
uri = URI.parse(raw)
127+
out[:scheme] = uri.scheme
128+
out[:host] = (uri.host || '').dup
129+
out[:port] = uri.port
130+
else
131+
host_part, port_part = raw.split(':', 2)
132+
out[:host] = host_part
133+
out[:port] = Integer(port_part) if port_part && port_part =~ /^\d+$/
134+
end
135+
136+
out
137+
rescue
138+
{ scheme: nil, host: raw, port: nil }
68139
end
69140

70-
def matches
71-
redis_uri_regex.match(@host)
141+
# Cluster when hostname begins with "clustercfg." or with "replication-group-" and isn't a nodeId leaf
142+
def infer_cluster_from_host(host)
143+
return false if host.to_s.empty?
144+
first = host.split('.').first
145+
return true if first == 'clustercfg'
146+
return true if first&.start_with?('replication-group-') && !host.include?('.nodeId.')
147+
false
72148
end
73149

74-
def port
75-
regex_port = matches[:port].to_i
76-
if regex_port.positive?
77-
regex_port
78-
else
79-
6379
80-
end
150+
# TLS implied by ElastiCache TLS endpoint hostnames:
151+
# - master.<replication-group>… (cluster mode disabled, TLS)
152+
# - clustercfg.<replication-group>… (cluster mode enabled, TLS)
153+
def infer_tls_from_host(host)
154+
return false if host.to_s.empty?
155+
first = host.split('.').first
156+
first == 'master' || first == 'clustercfg'
81157
end
82158

83-
def uri
84-
'redis' +
85-
-> { tls_mode? ? 's' : '' }.call +
86-
'://' +
87-
hostname +
88-
':' +
89-
port.to_s
159+
# A “zeroeth entry” (id==0) suggests more data; tests refer to this case explicitly
160+
def zeroeth_entry?(resp)
161+
first = resp.first
162+
return false unless first.is_a?(Array) && first.size >= 1
163+
first[0] == 0
164+
rescue
165+
false
90166
end
91167

92-
def redis_uri_regex
93-
%r{((?<scheme>redi[s]+)\://){0,1}(?<first>[0-9A-Za-z_-]+)\.(?<second>[0-9A-Za-z_-]+)\.{0,1}(?<third>[0-9A-Za-z_]*)\.(?<region>[0-9A-Za-z_-]+)\.cache\.amazonaws\.com:{0,1}(?<port>[0-9]*)}
168+
def to_bool(val)
169+
case val
170+
when true, false then val
171+
when Integer then val != 0
172+
else
173+
env_truthy?(val)
174+
end
94175
end
95176

96-
def tls_mode?
97-
matches[:scheme] == 'rediss' or
98-
%w[master clustercfg].include?(matches[:first])
177+
def env_truthy?(v)
178+
%w[true 1 yes on y].include?(v.to_s.strip.downcase)
99179
end
100180
end
101181
end

0 commit comments

Comments
 (0)