@@ -3,6 +3,7 @@ use std::collections::HashMap;
33use vrl:: value:: Kind ;
44
55use super :: {
6+ append_loki_path,
67 healthcheck:: healthcheck,
78 sink:: { LokiSink , confine_template_keys} ,
89} ;
@@ -12,7 +13,7 @@ use crate::{
1213 schema,
1314 sinks:: {
1415 prelude:: * ,
15- util:: { UriSerde , service:: TowerRequestSettings } ,
16+ util:: { HttpEndpoint , service:: TowerRequestSettings } ,
1617 } ,
1718 template:: { ConfinementConfig , Template , UnconfinedTemplate } ,
1819} ;
@@ -34,7 +35,7 @@ pub struct LokiConfig {
3435 ///
3536 /// The `path` value is appended to this.
3637 #[ configurable( metadata( docs:: examples = "http://localhost:3100" ) ) ]
37- pub endpoint : UriSerde ,
38+ pub endpoint : HttpEndpoint ,
3839
3940 /// The path to use in the URL of the Loki instance.
4041 #[ serde( default = "default_loki_path" ) ]
@@ -246,6 +247,11 @@ impl SinkConfig for LokiConfig {
246247
247248#[ derive( Clone , Debug ) ]
248249pub struct ValidatedLokiSink {
250+ /// The push URL: `endpoint` with `path` appended. Used by `LokiService`.
251+ pub ( super ) endpoint : HttpEndpoint ,
252+ /// The credential-free configured endpoint, including any user-supplied base path.
253+ /// The healthcheck appends `ready` or `/` to this URL.
254+ pub ( super ) base_endpoint : HttpEndpoint ,
249255 pub ( super ) auth : Option < Auth > ,
250256 pub ( super ) request_limits : TowerRequestSettings ,
251257 pub ( super ) transformer : Transformer ,
@@ -270,7 +276,11 @@ impl ValidatedSink for LokiConfig {
270276 }
271277 }
272278
273- let auth = self . auth . choose_one ( & self . endpoint . auth ) ?;
279+ // Extract basic-auth credentials embedded in the endpoint URL and strip
280+ // the userinfo, so credentials are sent as an `Authorization` header
281+ // rather than in the request URL.
282+ let ( base_endpoint, endpoint_auth) = self . endpoint . clone ( ) . extract_basic_auth ( ) ?;
283+ let auth = self . auth . choose_one ( & endpoint_auth) ?;
274284
275285 let request_limits = match self . out_of_order_action {
276286 OutOfOrderAction :: Accept => self . request . into_settings ( ) ,
@@ -302,17 +312,15 @@ impl ValidatedSink for LokiConfig {
302312 "structured_metadata[key]" ,
303313 ) ?;
304314
305- if !matches ! ( self . endpoint. uri. scheme_str( ) , Some ( "http" | "https" ) )
306- || self . endpoint . uri . authority ( ) . is_none ( )
307- {
308- return Err ( "Loki endpoint must be an absolute http(s) URL" . into ( ) ) ;
309- }
310-
311- self . endpoint . append_path ( & self . path ) ?;
315+ // The push URL appends `path`; the healthcheck appends `ready`/`` to the
316+ // configured base endpoint, so both are retained.
317+ let endpoint = append_loki_path ( & base_endpoint, & self . path ) ?;
312318
313319 let batch_settings = self . batch . into_batcher_settings ( ) ?;
314320
315321 Ok ( ValidatedLokiSink {
322+ endpoint,
323+ base_endpoint,
316324 auth,
317325 request_limits,
318326 transformer,
@@ -330,14 +338,14 @@ impl ValidatedSink for LokiConfig {
330338 ) -> crate :: Result < ( VectorSink , crate :: sinks:: Healthcheck ) > {
331339 let client = self . build_client ( cx) ?;
332340
333- let config = LokiConfig {
334- auth : validated. auth . clone ( ) ,
335- ..self . clone ( )
336- } ;
341+ let sink = LokiSink :: from_validated ( self , validated. clone ( ) , client. clone ( ) ) ?;
337342
338- let sink = LokiSink :: from_validated ( config. clone ( ) , validated. clone ( ) , client. clone ( ) ) ?;
339-
340- let healthcheck = healthcheck ( config, client) . boxed ( ) ;
343+ let healthcheck = healthcheck (
344+ validated. base_endpoint . clone ( ) ,
345+ validated. auth . clone ( ) ,
346+ client,
347+ )
348+ . boxed ( ) ;
341349 Ok ( ( VectorSink :: from_event_streamsink ( sink) , healthcheck) )
342350 }
343351}
@@ -434,6 +442,26 @@ mod tests {
434442 ) ;
435443 }
436444
445+ #[ test]
446+ fn deserialize_rejects_non_http_endpoint ( ) {
447+ let result = serde_yaml:: from_str :: < LokiConfig > (
448+ r#"
449+ endpoint: "ftp://localhost:3100"
450+ labels:
451+ test_name: "placeholder"
452+ encoding:
453+ codec: json
454+ "# ,
455+ ) ;
456+
457+ assert ! (
458+ result. is_err( ) ,
459+ "non-http endpoints must fail at config load"
460+ ) ;
461+ let message = result. unwrap_err ( ) . to_string ( ) ;
462+ assert ! ( message. contains( "ftp://localhost:3100" ) , "{message}" ) ;
463+ }
464+
437465 #[ test]
438466 fn validate_returns_usable_values ( ) {
439467 let config: LokiConfig = serde_yaml:: from_str (
@@ -454,10 +482,38 @@ mod tests {
454482 }
455483
456484 #[ test]
457- fn validate_rejects_non_http_endpoint ( ) {
485+ fn validate_extracts_endpoint_basic_auth ( ) {
486+ let config: LokiConfig = serde_yaml:: from_str (
487+ r#"
488+ endpoint: "http://user:pass@localhost:3100"
489+ labels:
490+ test_name: "placeholder"
491+ encoding:
492+ codec: json
493+ "# ,
494+ )
495+ . unwrap ( ) ;
496+
497+ let validated = config. validate ( ) . expect ( "validation should succeed" ) ;
498+ assert ! (
499+ validated. auth. is_some( ) ,
500+ "credentials embedded in the endpoint must be extracted as auth"
501+ ) ;
502+ assert ! (
503+ !validated. endpoint. to_string( ) . contains( '@' ) ,
504+ "userinfo must be stripped from the endpoint retained for build"
505+ ) ;
506+ }
507+
508+ #[ test]
509+ fn validate_rejects_conflicting_auth ( ) {
458510 let config: LokiConfig = serde_yaml:: from_str (
459511 r#"
460- endpoint: "ftp://example.com"
512+ endpoint: "http://user:pass@localhost:3100"
513+ auth:
514+ strategy: "basic"
515+ user: "other"
516+ password: "other"
461517 labels:
462518 test_name: "placeholder"
463519 encoding:
@@ -468,7 +524,7 @@ mod tests {
468524
469525 assert ! (
470526 config. validate( ) . is_err( ) ,
471- "a non-http(s) endpoint must be rejected before it reaches the HTTP client "
527+ "explicit auth and endpoint-embedded auth must not both be configured "
472528 ) ;
473529 }
474530
0 commit comments