1616using System . Buffers ;
1717using System . Diagnostics . CodeAnalysis ;
1818using System . Linq ;
19+ using System . Net ;
1920using System . Text ;
2021using System . Text . Json ;
2122using System . Threading ;
2223using System . Threading . Tasks ;
2324using Microsoft . AspNetCore . Builder ;
2425using Microsoft . AspNetCore . Http ;
2526using Microsoft . Net . Http . Headers ;
27+ using Seq . Api . Model . Shared ;
2628using SeqCli . Api ;
29+ using SeqCli . Config ;
2730using SeqCli . Forwarder . Channel ;
2831using SeqCli . Forwarder . Diagnostics ;
2932using JsonException = System . Text . Json . JsonException ;
3033
3134namespace SeqCli . Forwarder . Web . Api ;
3235
33- // ReSharper disable UnusedMethodReturnValue.Local
34-
3536class IngestionEndpoints : IMapEndpoints
3637{
3738 static readonly Encoding Utf8 = new UTF8Encoding ( false ) ;
3839
3940 readonly ForwardingChannelMap _forwardingChannels ;
41+ readonly SeqCliConfig _config ;
4042
41- public IngestionEndpoints ( ForwardingChannelMap forwardingChannels )
43+ public IngestionEndpoints ( ForwardingChannelMap forwardingChannels , SeqCliConfig config )
4244 {
4345 _forwardingChannels = forwardingChannels ;
46+ _config = config ;
4447 }
4548
4649 public void MapEndpoints ( WebApplication app )
4750 {
48- app . MapPost ( "ingest/clef" , async context => await IngestCompactFormatAsync ( context ) ) ;
49- app . MapPost ( "api/events/raw" , async context => await IngestAsync ( context ) ) ;
51+ app . MapPost ( "ingest/clef" , ( Delegate ) ( async ( HttpContext context ) => await IngestCompactFormatAsync ( context ) ) ) ;
52+ app . MapPost ( "api/events/raw" , ( Delegate ) ( async ( HttpContext context ) => await IngestAsync ( context ) ) ) ;
5053 }
5154
5255 async Task < IResult > IngestAsync ( HttpContext context )
@@ -62,7 +65,7 @@ async Task<IResult> IngestAsync(HttpContext context)
6265
6366 IngestionLog . ForClient ( context . Connection . RemoteIpAddress )
6467 . Error ( "Client supplied a legacy raw-format (non-CLEF) payload" ) ;
65- return Results . BadRequest ( "Only newline-delimited JSON (CLEF) payloads are supported." ) ;
68+ return Error ( HttpStatusCode . BadRequest , "Only newline-delimited JSON (CLEF) payloads are supported." ) ;
6669 }
6770
6871 async Task < IResult > IngestCompactFormatAsync ( HttpContext context )
@@ -74,7 +77,10 @@ async Task<IResult> IngestCompactFormatAsync(HttpContext context)
7477
7578 var log = _forwardingChannels . Get ( GetApiKey ( context . Request ) ) ;
7679
77- var payload = ArrayPool < byte > . Shared . Rent ( 1024 * 1024 * 10 ) ;
80+ // Add one for the extra newline that we have to insert at the end of batches.
81+ var bufferSize = _config . Connection . BatchSizeLimitBytes + 1 ;
82+ var rented = ArrayPool < byte > . Shared . Rent ( bufferSize ) ;
83+ var buffer = rented [ ..bufferSize ] ;
7884 var writeHead = 0 ;
7985 var readHead = 0 ;
8086
@@ -85,28 +91,37 @@ async Task<IResult> IngestCompactFormatAsync(HttpContext context)
8591 // size of write batches.
8692 while ( ! done )
8793 {
88- var remaining = payload . Length - writeHead ;
94+ var remaining = buffer . Length - 1 - writeHead ;
8995 if ( remaining == 0 )
9096 {
91- break ;
97+ IngestionLog . ForClient ( context . Connection . RemoteIpAddress )
98+ . Error ( "An incoming request exceeded the configured batch size limit" ) ;
99+ return Error ( HttpStatusCode . RequestEntityTooLarge , "the request is too large to process" ) ;
92100 }
93101
94- var read = await context . Request . Body . ReadAsync ( payload . AsMemory ( writeHead , remaining ) , cts . Token ) ;
102+ var read = await context . Request . Body . ReadAsync ( buffer . AsMemory ( writeHead , remaining ) , cts . Token ) ;
95103 if ( read == 0 )
96104 {
97105 done = true ;
98106 }
99107
100108 writeHead += read ;
109+
110+ // Ingested batches must be terminated with `\n`, but this isn't an API requirement.
111+ if ( done && writeHead > 0 && writeHead < buffer . Length && buffer [ writeHead - 1 ] != ( byte ) '\n ' )
112+ {
113+ buffer [ writeHead ] = ( byte ) '\n ' ;
114+ writeHead += 1 ;
115+ }
101116 }
102-
117+
103118 // Validate what we read, marking out a batch of one or more complete newline-delimited events.
104119 var batchStart = readHead ;
105120 var batchEnd = readHead ;
106121 while ( batchEnd < writeHead )
107122 {
108123 var eventStart = batchEnd ;
109- var nlIndex = payload . AsSpan ( ) [ eventStart ..] . IndexOf ( ( byte ) '\n ' ) ;
124+ var nlIndex = buffer . AsSpan ( ) [ eventStart ..] . IndexOf ( ( byte ) '\n ' ) ;
110125
111126 if ( nlIndex == - 1 )
112127 {
@@ -117,45 +132,41 @@ async Task<IResult> IngestCompactFormatAsync(HttpContext context)
117132
118133 batchEnd = eventEnd ;
119134 readHead = batchEnd ;
120-
121- if ( ! ValidateClef ( payload . AsSpan ( ) [ eventStart ..eventEnd ] , out var error ) )
135+
136+ if ( ! ValidateClef ( buffer . AsSpan ( ) [ eventStart ..eventEnd ] , out var error ) )
122137 {
123- var payloadText = Encoding . UTF8 . GetString ( payload . AsSpan ( ) [ eventStart ..eventEnd ] ) ;
138+ var payloadText = Encoding . UTF8 . GetString ( buffer . AsSpan ( ) [ eventStart ..eventEnd ] ) ;
124139 IngestionLog . ForPayload ( context . Connection . RemoteIpAddress , payloadText )
125140 . Error ( "Payload validation failed: {Error}" , error ) ;
126- return Results . BadRequest ( $ "Payload validation failed: { error } .") ;
141+ return Error ( HttpStatusCode . BadRequest , $ "Payload validation failed: { error } .") ;
127142 }
128143 }
129144
130145 if ( batchStart != batchEnd )
131146 {
132- await Write ( log , ArrayPool < byte > . Shared , payload , batchStart ..batchEnd , cts . Token ) ;
147+ await Write ( log , ArrayPool < byte > . Shared , buffer , batchStart ..batchEnd , cts . Token ) ;
133148 }
134149
135150 // Copy any unprocessed data into our buffer and continue
136- if ( ! done )
151+ if ( ! done && readHead != 0 )
137152 {
138153 var retain = writeHead - readHead ;
139- payload . AsSpan ( ) [ readHead ..writeHead ] . CopyTo ( payload . AsSpan ( ) [ ..retain ] ) ;
154+ buffer . AsSpan ( ) [ readHead ..writeHead ] . CopyTo ( buffer . AsSpan ( ) [ ..retain ] ) ;
140155 readHead = 0 ;
141156 writeHead = retain ;
142157 }
143158 }
144159
145160 // Exception cases are handled by `Write`
146- ArrayPool < byte > . Shared . Return ( payload ) ;
161+ ArrayPool < byte > . Shared . Return ( rented ) ;
147162
148- return TypedResults . Content (
149- null ,
150- "application/json" ,
151- Utf8 ,
152- StatusCodes . Status201Created ) ;
163+ return SuccessfulIngestion ( ) ;
153164 }
154165 catch ( Exception ex )
155166 {
156167 IngestionLog . ForClient ( context . Connection . RemoteIpAddress )
157168 . Error ( ex , "Ingestion failed" ) ;
158- return Results . InternalServerError ( ) ;
169+ return Error ( HttpStatusCode . InternalServerError , "Ingestion failed." ) ;
159170 }
160171 }
161172
@@ -185,11 +196,17 @@ static bool DefaultedBoolQuery(HttpRequest request, string queryParameterName)
185196 return request . Query . TryGetValue ( "apiKey" , out var apiKey ) ? apiKey . Last ( ) : null ;
186197 }
187198
188- static bool ValidateClef ( Span < byte > evt , [ NotNullWhen ( false ) ] out string ? errorFragment )
199+ bool ValidateClef ( Span < byte > evt , [ NotNullWhen ( false ) ] out string ? errorFragment )
189200 {
190201 // Note that `errorFragment` does not include user-supplied values; we opt in to adding this to
191202 // the ingestion log and include it using `ForPayload()`.
192203
204+ if ( evt . Length > _config . Connection . EventSizeLimitBytes )
205+ {
206+ errorFragment = "an event exceeds the configured size limit" ;
207+ return false ;
208+ }
209+
193210 var reader = new Utf8JsonReader ( evt ) ;
194211
195212 var foundTimestamp = false ;
@@ -225,7 +242,6 @@ static bool ValidateClef(Span<byte> evt, [NotNullWhen(false)] out string? errorF
225242 }
226243
227244 foundTimestamp = true ;
228- break ;
229245 }
230246 }
231247 }
@@ -259,4 +275,18 @@ static async Task Write(ForwardingChannel forwardingChannel, ArrayPool<byte> poo
259275 throw ;
260276 }
261277 }
262- }
278+
279+ static IResult Error ( HttpStatusCode statusCode , string message )
280+ {
281+ return Results . Json ( new ErrorPart { Error = message } , statusCode : ( int ) statusCode ) ;
282+ }
283+
284+ static IResult SuccessfulIngestion ( )
285+ {
286+ return TypedResults . Content (
287+ "{}" ,
288+ "application/json" ,
289+ Utf8 ,
290+ StatusCodes . Status201Created ) ;
291+ }
292+ }
0 commit comments