mirror of
https://github.com/neondatabase/neon.git
synced 2026-01-06 13:02:55 +00:00
feat(proxy): track SNI usage by protocol, including for http (#11863)
## Problem We want to see how many users of the legacy serverless driver are still using the old URL for SQL-over-HTTP traffic. ## Summary of changes Adds a protocol field to the connections_by_sni metric. Ensures it's incremented for sql-over-http.
This commit is contained in:
@@ -12,9 +12,9 @@ use tracing::{debug, warn};
|
|||||||
use crate::auth::password_hack::parse_endpoint_param;
|
use crate::auth::password_hack::parse_endpoint_param;
|
||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::error::{ReportableError, UserFacingError};
|
use crate::error::{ReportableError, UserFacingError};
|
||||||
use crate::metrics::{Metrics, SniKind};
|
use crate::metrics::{Metrics, SniGroup, SniKind};
|
||||||
use crate::proxy::NeonOptions;
|
use crate::proxy::NeonOptions;
|
||||||
use crate::serverless::SERVERLESS_DRIVER_SNI;
|
use crate::serverless::{AUTH_BROKER_SNI, SERVERLESS_DRIVER_SNI};
|
||||||
use crate::types::{EndpointId, RoleName};
|
use crate::types::{EndpointId, RoleName};
|
||||||
|
|
||||||
#[derive(Debug, Error, PartialEq, Eq, Clone)]
|
#[derive(Debug, Error, PartialEq, Eq, Clone)]
|
||||||
@@ -65,7 +65,7 @@ pub(crate) fn endpoint_sni(sni: &str, common_names: &HashSet<String>) -> Option<
|
|||||||
if !common_names.contains(common_name) {
|
if !common_names.contains(common_name) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if subdomain == SERVERLESS_DRIVER_SNI {
|
if subdomain == SERVERLESS_DRIVER_SNI || subdomain == AUTH_BROKER_SNI {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(EndpointId::from(subdomain))
|
Some(EndpointId::from(subdomain))
|
||||||
@@ -128,22 +128,23 @@ impl ComputeUserInfoMaybeEndpoint {
|
|||||||
|
|
||||||
let metrics = Metrics::get();
|
let metrics = Metrics::get();
|
||||||
debug!(%user, "credentials");
|
debug!(%user, "credentials");
|
||||||
if sni.is_some() {
|
|
||||||
|
let protocol = ctx.protocol();
|
||||||
|
let kind = if sni.is_some() {
|
||||||
debug!("Connection with sni");
|
debug!("Connection with sni");
|
||||||
metrics.proxy.accepted_connections_by_sni.inc(SniKind::Sni);
|
SniKind::Sni
|
||||||
} else if endpoint.is_some() {
|
} else if endpoint.is_some() {
|
||||||
metrics
|
|
||||||
.proxy
|
|
||||||
.accepted_connections_by_sni
|
|
||||||
.inc(SniKind::NoSni);
|
|
||||||
debug!("Connection without sni");
|
debug!("Connection without sni");
|
||||||
|
SniKind::NoSni
|
||||||
} else {
|
} else {
|
||||||
metrics
|
|
||||||
.proxy
|
|
||||||
.accepted_connections_by_sni
|
|
||||||
.inc(SniKind::PasswordHack);
|
|
||||||
debug!("Connection with password hack");
|
debug!("Connection with password hack");
|
||||||
}
|
SniKind::PasswordHack
|
||||||
|
};
|
||||||
|
|
||||||
|
metrics
|
||||||
|
.proxy
|
||||||
|
.accepted_connections_by_sni
|
||||||
|
.inc(SniGroup { protocol, kind });
|
||||||
|
|
||||||
let options = NeonOptions::parse_params(params);
|
let options = NeonOptions::parse_params(params);
|
||||||
|
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ pub struct ProxyMetrics {
|
|||||||
#[metric(metadata = Thresholds::with_buckets([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 10.0, 20.0, 50.0, 100.0]))]
|
#[metric(metadata = Thresholds::with_buckets([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 10.0, 20.0, 50.0, 100.0]))]
|
||||||
pub allowed_vpc_endpoint_ids: Histogram<10>,
|
pub allowed_vpc_endpoint_ids: Histogram<10>,
|
||||||
|
|
||||||
/// Number of connections (per sni).
|
/// Number of connections, by the method we used to determine the endpoint.
|
||||||
pub accepted_connections_by_sni: CounterVec<StaticLabelSet<SniKind>>,
|
pub accepted_connections_by_sni: CounterVec<SniSet>,
|
||||||
|
|
||||||
/// Number of connection failures (per kind).
|
/// Number of connection failures (per kind).
|
||||||
pub connection_failures_total: CounterVec<StaticLabelSet<ConnectionFailureKind>>,
|
pub connection_failures_total: CounterVec<StaticLabelSet<ConnectionFailureKind>>,
|
||||||
@@ -342,11 +342,20 @@ pub enum LatencyExclusions {
|
|||||||
ClientCplaneComputeRetry,
|
ClientCplaneComputeRetry,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(LabelGroup)]
|
||||||
|
#[label(set = SniSet)]
|
||||||
|
pub struct SniGroup {
|
||||||
|
pub protocol: Protocol,
|
||||||
|
pub kind: SniKind,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(FixedCardinalityLabel, Copy, Clone)]
|
#[derive(FixedCardinalityLabel, Copy, Clone)]
|
||||||
#[label(singleton = "kind")]
|
|
||||||
pub enum SniKind {
|
pub enum SniKind {
|
||||||
|
/// Domain name based routing. SNI for libpq/websockets. Host for HTTP
|
||||||
Sni,
|
Sni,
|
||||||
|
/// Metadata based routing. `options` for libpq/websockets. Header for HTTP
|
||||||
NoSni,
|
NoSni,
|
||||||
|
/// Metadata based routing, using the password field.
|
||||||
PasswordHack,
|
PasswordHack,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ use crate::serverless::backend::PoolingBackend;
|
|||||||
use crate::serverless::http_util::{api_error_into_response, json_response};
|
use crate::serverless::http_util::{api_error_into_response, json_response};
|
||||||
|
|
||||||
pub(crate) const SERVERLESS_DRIVER_SNI: &str = "api";
|
pub(crate) const SERVERLESS_DRIVER_SNI: &str = "api";
|
||||||
|
pub(crate) const AUTH_BROKER_SNI: &str = "apiauth";
|
||||||
|
|
||||||
pub async fn task_main(
|
pub async fn task_main(
|
||||||
config: &'static ProxyConfig,
|
config: &'static ProxyConfig,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ use crate::config::{AuthenticationConfig, HttpConfig, ProxyConfig, TlsConfig};
|
|||||||
use crate::context::RequestContext;
|
use crate::context::RequestContext;
|
||||||
use crate::error::{ErrorKind, ReportableError, UserFacingError};
|
use crate::error::{ErrorKind, ReportableError, UserFacingError};
|
||||||
use crate::http::{ReadBodyError, read_body_with_limit};
|
use crate::http::{ReadBodyError, read_body_with_limit};
|
||||||
use crate::metrics::{HttpDirection, Metrics};
|
use crate::metrics::{HttpDirection, Metrics, SniGroup, SniKind};
|
||||||
use crate::proxy::{NeonOptions, run_until_cancelled};
|
use crate::proxy::{NeonOptions, run_until_cancelled};
|
||||||
use crate::serverless::backend::HttpConnError;
|
use crate::serverless::backend::HttpConnError;
|
||||||
use crate::types::{DbName, RoleName};
|
use crate::types::{DbName, RoleName};
|
||||||
@@ -227,6 +227,32 @@ fn get_conn_info(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check the URL that was used, for metrics
|
||||||
|
{
|
||||||
|
let host_endpoint = headers
|
||||||
|
// get the host header
|
||||||
|
.get("host")
|
||||||
|
// extract the domain
|
||||||
|
.and_then(|h| {
|
||||||
|
let (host, _port) = h.to_str().ok()?.split_once(':')?;
|
||||||
|
Some(host)
|
||||||
|
})
|
||||||
|
// get the endpoint prefix
|
||||||
|
.map(|h| h.split_once('.').map_or(h, |(prefix, _)| prefix));
|
||||||
|
|
||||||
|
let kind = if host_endpoint == Some(&*endpoint) {
|
||||||
|
SniKind::Sni
|
||||||
|
} else {
|
||||||
|
SniKind::NoSni
|
||||||
|
};
|
||||||
|
|
||||||
|
let protocol = ctx.protocol();
|
||||||
|
Metrics::get()
|
||||||
|
.proxy
|
||||||
|
.accepted_connections_by_sni
|
||||||
|
.inc(SniGroup { protocol, kind });
|
||||||
|
}
|
||||||
|
|
||||||
ctx.set_user_agent(
|
ctx.set_user_agent(
|
||||||
headers
|
headers
|
||||||
.get(hyper::header::USER_AGENT)
|
.get(hyper::header::USER_AGENT)
|
||||||
|
|||||||
@@ -3835,7 +3835,7 @@ class NeonAuthBroker:
|
|||||||
external_http_port: int,
|
external_http_port: int,
|
||||||
auth_backend: NeonAuthBroker.ProxyV1,
|
auth_backend: NeonAuthBroker.ProxyV1,
|
||||||
):
|
):
|
||||||
self.domain = "apiauth.local.neon.build" # resolves to 127.0.0.1
|
self.domain = "local.neon.build" # resolves to 127.0.0.1
|
||||||
self.host = "127.0.0.1"
|
self.host = "127.0.0.1"
|
||||||
self.http_port = http_port
|
self.http_port = http_port
|
||||||
self.external_http_port = external_http_port
|
self.external_http_port = external_http_port
|
||||||
@@ -3852,7 +3852,7 @@ class NeonAuthBroker:
|
|||||||
# generate key of it doesn't exist
|
# generate key of it doesn't exist
|
||||||
crt_path = self.test_output_dir / "proxy.crt"
|
crt_path = self.test_output_dir / "proxy.crt"
|
||||||
key_path = self.test_output_dir / "proxy.key"
|
key_path = self.test_output_dir / "proxy.key"
|
||||||
generate_proxy_tls_certs("apiauth.local.neon.build", key_path, crt_path)
|
generate_proxy_tls_certs(f"apiauth.{self.domain}", key_path, crt_path)
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
str(self.neon_binpath / "proxy"),
|
str(self.neon_binpath / "proxy"),
|
||||||
@@ -3896,10 +3896,10 @@ class NeonAuthBroker:
|
|||||||
|
|
||||||
log.info(f"Executing http query: {query}")
|
log.info(f"Executing http query: {query}")
|
||||||
|
|
||||||
connstr = f"postgresql://{user}@{self.domain}/postgres"
|
connstr = f"postgresql://{user}@ep-foo-bar-1234.{self.domain}/postgres"
|
||||||
async with httpx.AsyncClient(verify=str(self.test_output_dir / "proxy.crt")) as client:
|
async with httpx.AsyncClient(verify=str(self.test_output_dir / "proxy.crt")) as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"https://{self.domain}:{self.external_http_port}/sql",
|
f"https://apiauth.{self.domain}:{self.external_http_port}/sql",
|
||||||
json={"query": query, "params": args},
|
json={"query": query, "params": args},
|
||||||
headers={
|
headers={
|
||||||
"Neon-Connection-String": connstr,
|
"Neon-Connection-String": connstr,
|
||||||
|
|||||||
Reference in New Issue
Block a user