Skip to content

Commit e96e4b0

Browse files
authored
feat(relay): Allow configuring separate http server for internal routes (#5352)
1 parent 4aa2585 commit e96e4b0

File tree

8 files changed

+165
-50
lines changed

8 files changed

+165
-50
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
**Features**:
66

77
- Only apply non-destructive PII rules to log bodies by default. ([#5272](https://github.com/getsentry/relay/pull/5272))
8+
- Allow configuring separate http server for internal routes. ([#5352](https://github.com/getsentry/relay/pull/5352))
89
- Infer the client ip when set to `{{auto}}` for EAP items. ([#5304](https://github.com/getsentry/relay/pull/5304))
910
- Increase the default size limit for attachments to 200MiB. ([#5310](https://github.com/getsentry/relay/pull/5310))
1011
- Add `sentry.origin` attribute to OTLP spans. ([#5294](https://github.com/getsentry/relay/pull/5294))

relay-config/src/config.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,24 @@ pub struct Relay {
487487
pub host: IpAddr,
488488
/// The port to bind for the unencrypted relay HTTP server.
489489
pub port: u16,
490+
/// The host the relay should bind to (network interface) for internally exposed APIs, like
491+
/// health checks.
492+
///
493+
/// If not configured, internal routes are exposed on the main HTTP server.
494+
///
495+
/// Note: configuring the internal http server on an address which overlaps with the main
496+
/// server (e.g. main on `0.0.0.0:3000` and internal on `127.0.0.1:3000`) is a misconfiguration
497+
/// resulting in approximately half of the requests sent to `127.0.0.1:3000` to fail, as the handling
498+
/// http server is chosen by the operating system 'at random'.
499+
///
500+
/// As a best practice you should always choose different ports to avoid this issue.
501+
///
502+
/// Defaults to [`Self::host`].
503+
pub internal_host: Option<IpAddr>,
504+
/// The port to bind for internally exposed APIs.
505+
///
506+
/// Defaults to [`Self::port`].
507+
pub internal_port: Option<u16>,
490508
/// Optional port to bind for the encrypted relay HTTPS server.
491509
#[serde(skip_serializing)]
492510
pub tls_port: Option<u16>,
@@ -512,6 +530,8 @@ impl Default for Relay {
512530
upstream: "https://sentry.io/".parse().unwrap(),
513531
host: default_host(),
514532
port: 3000,
533+
internal_host: None,
534+
internal_port: None,
515535
tls_port: None,
516536
tls_identity_path: None,
517537
tls_identity_password: None,
@@ -1948,6 +1968,25 @@ impl Config {
19481968
(self.values.relay.host, self.values.relay.port).into()
19491969
}
19501970

1971+
/// Returns the listen address for internal APIs.
1972+
///
1973+
/// Internal APIs are APIs which do not need to be publicly exposed,
1974+
/// like health checks.
1975+
///
1976+
/// Returns `None` when there is no explicit address configured for internal APIs,
1977+
/// and they should instead be exposed on the main [`Self::listen_addr`].
1978+
pub fn listen_addr_internal(&self) -> Option<SocketAddr> {
1979+
match (
1980+
self.values.relay.internal_host,
1981+
self.values.relay.internal_port,
1982+
) {
1983+
(Some(host), None) => Some((host, self.values.relay.port).into()),
1984+
(None, Some(port)) => Some((self.values.relay.host, port).into()),
1985+
(Some(host), Some(port)) => Some((host, port).into()),
1986+
(None, None) => None,
1987+
}
1988+
}
1989+
19511990
/// Returns the TLS listen address.
19521991
pub fn tls_listen_addr(&self) -> Option<SocketAddr> {
19531992
if self.values.relay.tls_identity_path.is_some() {

relay-server/src/endpoints/mod.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,35 @@ use crate::service::ServiceState;
3434
/// Size limit for internal batch endpoints.
3535
const BATCH_JSON_BODY_LIMIT: usize = 50_000_000; // 50 MB
3636

37+
/// All of Relay's routes.
38+
///
39+
/// This includes [`public_routes`] as well as [`internal_routes`].
40+
pub fn all_routes(config: &Config) -> Router<ServiceState> {
41+
public_routes_raw(config).merge(internal_routes(config))
42+
}
43+
44+
/// Relay's internal routes.
45+
///
46+
/// Routes which do not need to be exposed.
3747
#[rustfmt::skip]
38-
pub fn routes(config: &Config) -> Router<ServiceState>{
39-
// Relay-internal routes pointing to /api/relay/
40-
let internal_routes = Router::new()
48+
pub fn internal_routes(_: &Config) -> Router<ServiceState>{
49+
Router::new()
4150
.route("/api/relay/healthcheck/{kind}/", get(health_check::handle))
4251
.route("/api/relay/autoscaling/", get(autoscaling::handle))
4352
// Fallback route, but with a name, and just on `/api/relay/*`.
44-
.route("/api/relay/{*not_found}", any(statics::not_found));
53+
.route("/api/relay/{*not_found}", any(statics::not_found))
54+
}
4555

56+
/// Relay's public routes.
57+
///
58+
/// Routes which are public API and must be exposed.
59+
pub fn public_routes(config: &Config) -> Router<ServiceState> {
60+
// Exclude internal routes, they must be configured separately.
61+
public_routes_raw(config).route("/api/relay/{*not_found}", any(statics::not_found))
62+
}
63+
64+
#[rustfmt::skip]
65+
fn public_routes_raw(config: &Config) -> Router<ServiceState> {
4666
// Sentry Web API routes pointing to /api/0/relays/
4767
let web_routes = Router::new()
4868
.route("/api/0/relays/projectconfigs/", post(project_configs::handle))
@@ -93,7 +113,7 @@ pub fn routes(config: &Config) -> Router<ServiceState>{
93113
// NOTE: If you add a new (non-experimental) route here, please also list it in
94114
// https://github.com/getsentry/sentry-docs/blob/master/docs/product/relay/operating-guidelines.mdx
95115

96-
Router::new().merge(internal_routes)
116+
Router::new()
97117
.merge(web_routes)
98118
.merge(batch_routes)
99119
.merge(store_routes)

relay-server/src/services/server/mod.rs

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub enum ServerError {
5858
type App = NormalizePath<axum::Router>;
5959

6060
/// Build the axum application with all routes and middleware.
61-
fn make_app(service: ServiceState) -> App {
61+
fn make_app(service: ServiceState, f: impl FnOnce(&Config) -> axum::Router<ServiceState>) -> App {
6262
// Build the router middleware into a single service which runs _after_ routing. Service
6363
// builder order defines layers added first will be called first. This means:
6464
// - Requests go from top to bottom
@@ -85,25 +85,14 @@ fn make_app(service: ServiceState) -> App {
8585
.compress_when(SizeAbove::new(COMPRESSION_MIN_SIZE).and(DefaultPredicate::new())),
8686
);
8787

88-
let router = crate::endpoints::routes(service.config())
89-
.layer(middleware)
90-
.with_state(service);
88+
let router = f(service.config()).layer(middleware).with_state(service);
9189

9290
// Add middlewares that need to run _before_ routing, which need to wrap the router. This are
9391
// especially middlewares that modify the request path for the router:
9492
NormalizePath::new(router)
9593
}
9694

97-
fn listen(config: &Config) -> Result<TcpListener, ServerError> {
98-
// Inform the user about a removed feature.
99-
if config.tls_listen_addr().is_some()
100-
|| config.tls_identity_password().is_some()
101-
|| config.tls_identity_path().is_some()
102-
{
103-
return Err(ServerError::TlsNotSupported);
104-
}
105-
106-
let addr = config.listen_addr();
95+
fn listen(addr: SocketAddr, config: &Config) -> Result<TcpListener, ServerError> {
10796
let socket = match addr {
10897
SocketAddr::V4(_) => TcpSocket::new_v4(),
10998
SocketAddr::V6(_) => TcpSocket::new_v6(),
@@ -115,7 +104,7 @@ fn listen(config: &Config) -> Result<TcpListener, ServerError> {
115104
Ok(socket.listen(config.tcp_listen_backlog())?.into_std()?)
116105
}
117106

118-
async fn serve(listener: TcpListener, app: App, config: Arc<Config>) {
107+
async fn serve(listener: TcpListener, app: App, config: &Config) -> std::io::Result<()> {
119108
let handle = Handle::new();
120109

121110
let acceptor = self::acceptor::RelayAcceptor::new()
@@ -162,10 +151,7 @@ async fn serve(listener: TcpListener, app: App, config: Arc<Config>) {
162151
}
163152
});
164153

165-
server
166-
.serve(service)
167-
.await
168-
.expect("failed to start axum server");
154+
server.serve(service).await
169155
}
170156

171157
/// HTTP server service.
@@ -176,16 +162,30 @@ pub struct HttpServer {
176162
config: Arc<Config>,
177163
service: ServiceState,
178164
listener: TcpListener,
165+
internal_listener: Option<TcpListener>,
179166
}
180167

181168
impl HttpServer {
182169
pub fn new(config: Arc<Config>, service: ServiceState) -> Result<Self, ServerError> {
183-
let listener = listen(&config)?;
170+
// Inform the user about a removed feature.
171+
if config.tls_listen_addr().is_some()
172+
|| config.tls_identity_password().is_some()
173+
|| config.tls_identity_path().is_some()
174+
{
175+
return Err(ServerError::TlsNotSupported);
176+
}
177+
178+
let listener = listen(config.listen_addr(), &config)?;
179+
let internal_listener = match config.listen_addr_internal() {
180+
Some(addr) => Some(listen(addr, &config)?),
181+
None => None,
182+
};
184183

185184
Ok(Self {
186185
config,
187186
service,
188187
listener,
188+
internal_listener,
189189
})
190190
}
191191
}
@@ -198,14 +198,32 @@ impl Service for HttpServer {
198198
config,
199199
service,
200200
listener,
201+
internal_listener,
201202
} = self;
202203

204+
let listen_addr = config.listen_addr();
205+
203206
relay_log::info!("spawning http server");
204-
relay_log::info!(" listening on http://{}/", config.listen_addr());
207+
relay_log::info!(" listening on http://{listen_addr}/");
208+
if let Some(internal_addr) = config.listen_addr_internal() {
209+
relay_log::info!(" listening on http://{internal_addr}/ [internal]");
210+
}
205211
relay_statsd::metric!(counter(RelayCounters::ServerStarting) += 1);
206212

207-
let app = make_app(service);
208-
serve(listener, app, config).await;
213+
if let Some(internal_listener) = internal_listener {
214+
let public = make_app(service.clone(), crate::endpoints::public_routes);
215+
let internal = make_app(service, crate::endpoints::internal_routes);
216+
217+
tokio::try_join!(
218+
serve(listener, public, &config),
219+
serve(internal_listener, internal, &config),
220+
)
221+
.map(drop)
222+
} else {
223+
let app = make_app(service, crate::endpoints::all_routes);
224+
serve(listener, app, &config).await
225+
}
226+
.expect("axum listener to not fail")
209227
}
210228
}
211229

@@ -214,10 +232,13 @@ async fn emit_active_connections_metric(interval: Option<Duration>, handle: Hand
214232
return;
215233
};
216234

235+
let addr = handle.listening().await.map(|addr| addr.to_string());
236+
217237
loop {
218238
ticker.tick().await;
219239
relay_statsd::metric!(
220-
gauge(RelayGauges::ServerActiveConnections) = handle.connection_count() as u64
240+
gauge(RelayGauges::ServerActiveConnections) = handle.connection_count() as u64,
241+
addr = addr.as_deref().unwrap_or("unknown"),
221242
);
222243
}
223244
}

relay/src/healthcheck.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub fn healthcheck(config: &Config, matches: &ArgMatches) -> Result<()> {
1818
let addr = matches
1919
.get_one::<SocketAddr>("addr")
2020
.copied()
21+
.or_else(|| config.listen_addr_internal())
2122
.unwrap_or(config.listen_addr());
2223

2324
let client = Client::builder()

tests/integration/fixtures/__init__.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,15 @@ class SentryLike:
1313

1414
default_dsn_public_key = "31a5a894b4524f74a9a8d0e27e21ba91"
1515

16-
def __init__(self, server_address, upstream=None, public_key=None):
16+
def __init__(
17+
self,
18+
server_address,
19+
upstream=None,
20+
public_key=None,
21+
internal_server_address=None,
22+
):
1723
self.server_address = server_address
24+
self.internal_server_address = internal_server_address or server_address
1825
self.upstream = upstream
1926
self.public_key = public_key
2027

@@ -79,6 +86,10 @@ def get_dsn(self, project_id, index=0):
7986
def url(self):
8087
return "http://{}:{}".format(*self.server_address)
8188

89+
@property
90+
def internal_url(self):
91+
return "http://{}:{}".format(*self.internal_server_address)
92+
8293
def get_auth_header(self, project_id, dsn_key_idx=0, dsn_key=None):
8394
if dsn_key is None:
8495
dsn_key = self.get_dsn_public_key(project_id, dsn_key_idx)
@@ -88,11 +99,11 @@ def get_auth_header(self, project_id, dsn_key_idx=0, dsn_key=None):
8899
"sentry_key={}".format(dsn_key)
89100
)
90101

91-
def _wait(self, path):
102+
def _wait(self, path, *, is_internal=False):
92103
backoff = 0.1
93104
while True:
94105
try:
95-
self.get(path).raise_for_status()
106+
self.get(path, is_internal=is_internal).raise_for_status()
96107
break
97108
except Exception:
98109
time.sleep(backoff)
@@ -104,7 +115,7 @@ def wait_relay_health_check(self):
104115
if self._health_check_passed:
105116
return
106117

107-
self._wait("/api/relay/healthcheck/ready/")
118+
self._wait("/api/relay/healthcheck/ready/", is_internal=True)
108119
self._health_check_passed = True
109120

110121
def __repr__(self):
@@ -551,13 +562,14 @@ def send_check_in(self, project_id, check_in):
551562
envelope.add_item(Item(payload=PayloadRef(json=check_in), type="check_in"))
552563
self.send_envelope(project_id, envelope)
553564

554-
def request(self, method, path, timeout=None, **kwargs):
565+
def request(self, method, path, timeout=None, is_internal=False, **kwargs):
555566
assert path.startswith("/")
556567

557568
if timeout is None:
558569
timeout = 10
559570

560-
return self.session.request(method, self.url + path, timeout=timeout, **kwargs)
571+
url = self.url if not is_internal else self.internal_url
572+
return self.session.request(method, url + path, timeout=timeout, **kwargs)
561573

562574
def post(self, path, **kwargs):
563575
return self.request("post", path, **kwargs)

tests/integration/fixtures/relay.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ def __init__(
3333
options,
3434
version,
3535
):
36-
super().__init__(server_address, upstream, public_key)
36+
super().__init__(
37+
server_address,
38+
upstream,
39+
public_key,
40+
internal_server_address=get_internal_address(options, server_address),
41+
)
3742

3843
self.process = process
3944
self.relay_id = relay_id
@@ -242,3 +247,10 @@ def latest_relay_version(get_relay_binary):
242247
).strip()
243248
_the_word_relay, version = version_str.split(" ", 1)
244249
return version
250+
251+
252+
def get_internal_address(options, server_address):
253+
relay = (options or {}).get("relay", {})
254+
host = relay.get("internal_host")
255+
port = relay.get("internal_port")
256+
return (host or server_address[0], port or server_address[1])

0 commit comments

Comments
 (0)