1use crate::{
11 ast::{
12 ArithOp, BoolOp, ChangeKind, CheckQuery, CmpOp, CompositeExpr, Condition, Filter, FuncKind,
13 FuncParam, MetricExpr, MetricQuery, Modifier, MonitorQuery, MonitorRef, ParamValue, Scalar,
14 SearchQuery, SearchSource, Series, SloQuery, SpaceAgg, TimeAgg, Window,
15 },
16 cursor::Cursor,
17 error::ParseError,
18};
19
20const MAX_DEPTH: usize = 128;
23
24pub fn parse(query: &str) -> Result<MonitorQuery, ParseError> {
32 if query.trim().is_empty() {
33 return Err(ParseError::new(0, "empty query"));
34 }
35 let mut parser = Parser::new(query);
36 let parsed = parser.parse_query()?;
37 parser.cursor.skip_ws();
38 if !parser.cursor.is_done() {
39 return Err(parser.cursor.error("unexpected trailing input"));
40 }
41 Ok(parsed)
42}
43
44fn is_ident(c: char) -> bool {
47 c.is_ascii_alphanumeric() || c == '_'
48}
49
50struct Parser<'a> {
51 cursor: Cursor<'a>,
52}
53
54impl<'a> Parser<'a> {
55 fn new(input: &'a str) -> Self {
56 Self {
57 cursor: Cursor::new(input),
58 }
59 }
60
61 fn parse_query(&mut self) -> Result<MonitorQuery, ParseError> {
63 self.cursor.skip_ws();
64 match self.cursor.peek() {
65 Some('"') => self.parse_check().map(MonitorQuery::ServiceCheck),
66 Some(c) if c.is_ascii_digit() || c == '!' || c == '(' => {
67 self.parse_composite(0).map(MonitorQuery::Composite)
68 }
69 Some(_) => {
70 let head = self.peek_ident();
71 match head.as_str() {
72 "error_budget" => self.parse_slo().map(MonitorQuery::Slo),
73 "logs" | "events" | "rum" | "error-tracking" => {
74 self.parse_search().map(MonitorQuery::Search)
75 }
76 "avg" | "sum" | "min" | "max" | "count" | "percentile" | "change"
77 | "pct_change" => self.parse_metric().map(MonitorQuery::Metric),
78 "" => Err(self.cursor.error("unrecognized query")),
79 other => Err(self
80 .cursor
81 .error(format!("unrecognized query head `{other}`"))),
82 }
83 }
84 None => Err(self.cursor.error("empty query")),
85 }
86 }
87
88 fn peek_ident(&self) -> String {
91 let mut probe = self.cursor.clone();
92 probe.take_while(|c| is_ident(c) || c == '-').to_string()
93 }
94
95 fn check_depth(&self, depth: usize) -> Result<(), ParseError> {
96 if depth > MAX_DEPTH {
97 Err(self.cursor.error("maximum nesting depth exceeded"))
98 } else {
99 Ok(())
100 }
101 }
102
103 fn parse_metric(&mut self) -> Result<MetricQuery, ParseError> {
106 let head = self.take_ident()?;
107 if let Some(kind) = ChangeKind::from_token(&head) {
108 return self.parse_change_metric(kind);
109 }
110 let time_aggregation = TimeAgg::from_token(&head).ok_or_else(|| {
111 self.cursor
112 .error(format!("invalid time aggregation `{head}`"))
113 })?;
114 self.cursor.expect("(")?;
115 let window = self.take_window()?;
116 self.cursor.expect(")")?;
117 self.cursor.expect(":")?;
118 let expr = self.parse_arith(0, 0)?;
119 let condition = self.parse_condition()?;
120 Ok(MetricQuery {
121 time_aggregation,
122 window,
123 expr,
124 condition,
125 })
126 }
127
128 fn parse_change_metric(&mut self, kind: ChangeKind) -> Result<MetricQuery, ParseError> {
132 self.cursor.expect("(")?;
133 let mut probe = self.cursor.clone();
137 let head = probe.take_while(is_ident);
138 let is_full_form = TimeAgg::from_token(head).is_some() && probe.peek() == Some('(');
139
140 let (inner_agg, window, shift) = if is_full_form {
141 let inner = self.take_ident()?;
142 let inner_agg = TimeAgg::from_token(&inner).ok_or_else(|| {
143 self.cursor
144 .error(format!("invalid time aggregation `{inner}`"))
145 })?;
146 self.cursor.expect("(")?;
147 let window = self.take_window()?;
148 self.cursor.expect(")")?;
149 self.cursor.expect(",")?;
150 let shift = self.take_window()?;
151 (inner_agg, window, shift)
152 } else {
153 let window = self.take_window()?;
156 (TimeAgg::Avg, window.clone(), window)
157 };
158 self.cursor.expect(")")?;
159 self.cursor.expect(":")?;
160 let inner_expr = self.parse_arith(0, 0)?;
161 let condition = self.parse_condition()?;
162 let expr = MetricExpr::Change {
163 kind,
164 inner_agg,
165 shift,
166 arg: Box::new(inner_expr),
167 };
168 Ok(MetricQuery {
169 time_aggregation: inner_agg,
170 window,
171 expr,
172 condition,
173 })
174 }
175
176 fn parse_arith(&mut self, min_bp: u8, depth: usize) -> Result<MetricExpr, ParseError> {
178 self.check_depth(depth)?;
179 let mut lhs = self.parse_term(depth)?;
180 while let Some(op) = self.peek_arith_op() {
181 let (lbp, rbp) = binding_power(op);
182 if lbp < min_bp {
183 break;
184 }
185 self.cursor.eat(op.as_token());
186 let rhs = self.parse_arith(rbp, depth + 1)?;
187 lhs = MetricExpr::Arith {
188 op,
189 lhs: Box::new(lhs),
190 rhs: Box::new(rhs),
191 };
192 }
193 Ok(lhs)
194 }
195
196 fn peek_arith_op(&self) -> Option<ArithOp> {
197 match self.cursor.peek()? {
198 '+' => Some(ArithOp::Add),
199 '-' => Some(ArithOp::Sub),
200 '*' => Some(ArithOp::Mul),
201 '/' => Some(ArithOp::Div),
202 _ => None,
203 }
204 }
205
206 fn parse_term(&mut self, depth: usize) -> Result<MetricExpr, ParseError> {
207 self.check_depth(depth)?;
208 let c = self
209 .cursor
210 .peek()
211 .ok_or_else(|| self.cursor.error("unexpected end of expression"))?;
212 if c == '(' {
213 self.cursor.expect("(")?;
214 let inner = self.parse_arith(0, depth + 1)?;
215 self.cursor.expect(")")?;
216 return Ok(inner);
217 }
218 if c.is_ascii_digit() || c == '+' || c == '-' || c == '.' {
219 return Ok(MetricExpr::Scalar(Scalar::new(self.cursor.take_number()?)));
220 }
221 if let Some(rank) = self.try_take_percentile_prefix() {
224 return Ok(MetricExpr::Series(
225 self.parse_series(SpaceAgg::Percentile(rank))?,
226 ));
227 }
228 let id = self.take_ident()?;
229 if let Some(func) = FuncKind::from_token(&id)
230 && self.cursor.peek() == Some('(')
231 {
232 return self.parse_function(func, depth);
233 }
234 if self.cursor.peek() == Some(':') {
235 let space_aggregation = SpaceAgg::from_token(&id).ok_or_else(|| {
236 self.cursor
237 .error(format!("invalid space aggregation `{id}`"))
238 })?;
239 self.cursor.expect(":")?;
240 return Ok(MetricExpr::Series(self.parse_series(space_aggregation)?));
241 }
242 if self.cursor.peek() == Some('(') {
243 if SpaceAgg::from_token(&id).is_some() {
247 return self.parse_combiner(id, depth);
248 }
249 return self.parse_transform(id, depth);
250 }
251 if matches!(self.cursor.peek(), Some('.' | '{')) {
256 let metric = self.continue_metric_name(id)?;
257 return Ok(MetricExpr::Series(
258 self.parse_series_from_metric(SpaceAgg::Avg, metric)?,
259 ));
260 }
261 Err(self
262 .cursor
263 .error(format!("expected `:` or `(` after `{id}`")))
264 }
265
266 fn parse_combiner(&mut self, name: String, depth: usize) -> Result<MetricExpr, ParseError> {
269 self.cursor.expect("(")?;
270 let mut args = vec![self.parse_arith(0, depth + 1)?];
271 while self.cursor.eat(",") {
272 args.push(self.parse_arith(0, depth + 1)?);
273 }
274 self.cursor.expect(")")?;
275 Ok(MetricExpr::Combine { name, args })
276 }
277
278 fn parse_function(&mut self, name: FuncKind, depth: usize) -> Result<MetricExpr, ParseError> {
279 self.cursor.expect("(")?;
280 let arg = self.parse_arith(0, depth + 1)?;
281 let mut params = Vec::new();
282 while self.cursor.eat(",") {
283 params.push(self.parse_func_param()?);
284 }
285 self.cursor.expect(")")?;
286 Ok(MetricExpr::Function {
287 name,
288 arg: Box::new(arg),
289 params,
290 })
291 }
292
293 fn parse_func_param(&mut self) -> Result<FuncParam, ParseError> {
294 match self.cursor.peek() {
295 Some('\'' | '"') => Ok(FuncParam::positional(ParamValue::Str(
296 self.cursor.take_quoted()?,
297 ))),
298 Some(c) if c.is_ascii_digit() || c == '+' || c == '-' || c == '.' => Ok(
299 FuncParam::positional(ParamValue::Number(self.cursor.take_number()?)),
300 ),
301 Some(_) => {
302 let key = self.take_ident()?;
303 if self.cursor.eat("=") {
304 Ok(FuncParam::keyword(key, self.parse_param_value()?))
305 } else {
306 Ok(FuncParam::positional(bareword_value(&key)))
307 }
308 }
309 None => Err(self.cursor.error("expected a function parameter")),
310 }
311 }
312
313 fn parse_param_value(&mut self) -> Result<ParamValue, ParseError> {
314 match self.cursor.peek() {
315 Some('\'' | '"') => Ok(ParamValue::Str(self.cursor.take_quoted()?)),
316 Some(c) if c.is_ascii_digit() || c == '+' || c == '-' || c == '.' => {
317 Ok(ParamValue::Number(self.cursor.take_number()?))
318 }
319 Some(_) => Ok(bareword_value(&self.take_ident()?)),
320 None => Err(self.cursor.error("expected a parameter value")),
321 }
322 }
323
324 fn parse_transform(&mut self, name: String, depth: usize) -> Result<MetricExpr, ParseError> {
325 self.cursor.expect("(")?;
326 let arg = self.parse_arith(0, depth + 1)?;
327 let mut args = Vec::new();
328 while self.cursor.eat(",") {
329 args.push(self.parse_param_value()?);
332 }
333 self.cursor.expect(")")?;
334 Ok(MetricExpr::Transform {
335 name,
336 arg: Box::new(arg),
337 args,
338 })
339 }
340
341 fn parse_series(&mut self, space_aggregation: SpaceAgg) -> Result<Series, ParseError> {
342 let metric = self.take_metric_name()?;
343 self.parse_series_from_metric(space_aggregation, metric)
344 }
345
346 fn parse_series_from_metric(
350 &mut self,
351 space_aggregation: SpaceAgg,
352 metric: String,
353 ) -> Result<Series, ParseError> {
354 let filter = if self.cursor.peek() == Some('{') {
355 self.cursor.expect("{")?;
356 let filters = self.parse_filter_list();
357 self.cursor.expect("}")?;
358 filters
359 } else {
360 Vec::new()
361 };
362 let group_by = self.parse_group_by()?;
363 let modifiers = self.parse_modifiers()?;
364 Ok(Series {
365 space_aggregation,
366 metric,
367 filter,
368 group_by,
369 modifiers,
370 })
371 }
372
373 fn parse_filter_list(&mut self) -> Vec<Filter> {
374 let mut filters = Vec::new();
375 loop {
376 self.skip_filter_seps();
377 if matches!(self.cursor.peek(), Some('}') | None) {
378 break;
379 }
380 let token = self
381 .cursor
382 .take_while(|c| c != ',' && c != '}' && !c.is_whitespace());
383 if token.is_empty() {
384 break;
385 }
386 filters.push(classify_filter(token));
387 }
388 filters
389 }
390
391 fn skip_filter_seps(&mut self) {
393 loop {
394 self.cursor.skip_ws();
395 if !self.cursor.eat(",") {
396 break;
397 }
398 }
399 }
400
401 fn parse_group_by(&mut self) -> Result<Vec<String>, ParseError> {
402 self.cursor.skip_ws();
403 let rest = self.cursor.rest();
404 let is_by = rest
405 .strip_prefix("by")
406 .is_some_and(|after| after.starts_with(|c: char| c.is_whitespace() || c == '{'));
407 if !is_by {
408 return Ok(Vec::new());
409 }
410 self.cursor.eat("by");
411 self.cursor.expect("{")?;
412 let mut tags = Vec::new();
413 loop {
414 self.skip_filter_seps();
415 if self.cursor.peek() == Some('}') || self.cursor.peek().is_none() {
416 break;
417 }
418 let tag = self
419 .cursor
420 .take_while(|c| c != ',' && c != '}' && !c.is_whitespace());
421 if tag.is_empty() {
422 break;
423 }
424 tags.push(tag.to_string());
425 }
426 self.cursor.expect("}")?;
427 Ok(tags)
428 }
429
430 fn parse_modifiers(&mut self) -> Result<Vec<Modifier>, ParseError> {
431 let mut modifiers = Vec::new();
432 loop {
433 self.cursor.skip_ws();
434 if !self.cursor.rest().starts_with('.') {
438 break;
439 }
440 self.cursor.expect(".")?;
441 let name = self.take_ident()?;
442 self.cursor.expect("(")?;
443 let args = self.parse_string_args()?;
444 modifiers.push(Modifier { name, args });
445 }
446 Ok(modifiers)
447 }
448
449 fn parse_search(&mut self) -> Result<SearchQuery, ParseError> {
452 let source = self.take_source()?;
453 self.cursor.expect("(")?;
454 let raw_search = self.cursor.take_quoted()?;
455 self.cursor.expect(")")?;
456
457 let mut index = None;
458 let mut rollup_method = None;
459 let mut rollup_arg = None;
460 let mut group_by = Vec::new();
461 let mut last = None;
462
463 while self.cursor.eat(".") {
464 let method = self.take_ident()?;
465 self.cursor.expect("(")?;
466 let mut args = self.parse_string_args()?;
467 match method.as_str() {
468 "index" => index = args.into_iter().next().filter(|s| !s.is_empty()),
469 "rollup" => {
470 let mut it = args.into_iter();
471 rollup_method = it.next();
472 rollup_arg = it.next();
473 }
474 "by" => group_by = args,
475 "last" => last = args.drain(..).next(),
476 other => {
477 return Err(self
478 .cursor
479 .error(format!("unknown search method `{other}`")));
480 }
481 }
482 }
483
484 let rollup_method = rollup_method
485 .ok_or_else(|| self.cursor.error("search query missing `.rollup(...)`"))?;
486 let last = last.ok_or_else(|| self.cursor.error("search query missing `.last(...)`"))?;
487 let condition = self.parse_condition()?;
488 Ok(SearchQuery {
489 source,
490 raw_search,
491 index,
492 rollup_method,
493 rollup_arg,
494 group_by,
495 last,
496 condition,
497 })
498 }
499
500 fn take_source(&mut self) -> Result<SearchSource, ParseError> {
501 let id = self.cursor.take_while(|c| is_ident(c) || c == '-');
502 match id {
503 "logs" => Ok(SearchSource::Logs),
504 "events" => Ok(SearchSource::Events),
505 "rum" => Ok(SearchSource::Rum),
506 "error-tracking" => Ok(SearchSource::ErrorTracking),
507 other => Err(self
508 .cursor
509 .error(format!("invalid search source `{other}`"))),
510 }
511 }
512
513 fn parse_check(&mut self) -> Result<CheckQuery, ParseError> {
516 let check = self.cursor.take_quoted()?;
517 let mut over = Vec::new();
518 let mut by = Vec::new();
519 let mut last = None;
520 let mut saw_count = false;
521
522 while self.cursor.eat(".") {
523 let method = self.take_ident()?;
524 self.cursor.expect("(")?;
525 match method.as_str() {
526 "over" => over = self.parse_string_args()?,
527 "by" => by = self.parse_string_args()?,
528 "last" => {
529 let n = self.cursor.take_number()?;
530 self.cursor.expect(")")?;
531 last = Some(checked_u32(n).ok_or_else(|| {
532 self.cursor
533 .error("`.last(n)` must be a non-negative integer")
534 })?);
535 }
536 "count_by_status" => {
537 self.parse_string_args()?;
538 saw_count = true;
539 }
540 other => {
541 return Err(self.cursor.error(format!("unknown check method `{other}`")));
542 }
543 }
544 }
545
546 let last = last.ok_or_else(|| self.cursor.error("check query missing `.last(n)`"))?;
547 if !saw_count {
548 return Err(self
549 .cursor
550 .error("check query missing `.count_by_status()`"));
551 }
552 Ok(CheckQuery {
553 check,
554 over,
555 by,
556 last,
557 })
558 }
559
560 fn parse_slo(&mut self) -> Result<SloQuery, ParseError> {
563 self.cursor.expect("error_budget")?;
564 self.cursor.expect("(")?;
565 let id = self.cursor.take_quoted()?;
566 self.cursor.expect(")")?;
567 self.cursor.expect(".")?;
568 let method = self.take_ident()?;
569 if method != "over" {
570 return Err(self
571 .cursor
572 .error(format!("expected `.over(...)`, found `.{method}`")));
573 }
574 self.cursor.expect("(")?;
575 let over = self.cursor.take_quoted()?;
576 self.cursor.expect(")")?;
577 let condition = self.parse_condition()?;
578 Ok(SloQuery {
579 id,
580 over,
581 condition,
582 })
583 }
584
585 fn parse_composite(&mut self, depth: usize) -> Result<CompositeExpr, ParseError> {
588 self.parse_composite_or(depth)
589 }
590
591 fn parse_composite_or(&mut self, depth: usize) -> Result<CompositeExpr, ParseError> {
592 self.check_depth(depth)?;
593 let mut lhs = self.parse_composite_and(depth)?;
594 while self.cursor.eat("||") {
595 let rhs = self.parse_composite_and(depth + 1)?;
596 lhs = CompositeExpr::Binary {
597 op: BoolOp::Or,
598 lhs: Box::new(lhs),
599 rhs: Box::new(rhs),
600 };
601 }
602 Ok(lhs)
603 }
604
605 fn parse_composite_and(&mut self, depth: usize) -> Result<CompositeExpr, ParseError> {
606 self.check_depth(depth)?;
607 let mut lhs = self.parse_composite_unary(depth)?;
608 while self.cursor.eat("&&") {
609 let rhs = self.parse_composite_unary(depth + 1)?;
610 lhs = CompositeExpr::Binary {
611 op: BoolOp::And,
612 lhs: Box::new(lhs),
613 rhs: Box::new(rhs),
614 };
615 }
616 Ok(lhs)
617 }
618
619 fn parse_composite_unary(&mut self, depth: usize) -> Result<CompositeExpr, ParseError> {
620 self.check_depth(depth)?;
621 if self.cursor.eat("!") {
622 return Ok(CompositeExpr::Not(Box::new(
623 self.parse_composite_unary(depth + 1)?,
624 )));
625 }
626 if self.cursor.eat("(") {
627 let inner = self.parse_composite_or(depth + 1)?;
628 self.cursor.expect(")")?;
629 return Ok(inner);
630 }
631 let id = self.cursor.take_while(|c| c.is_ascii_digit());
632 if id.is_empty() {
633 return Err(self.cursor.error("expected a monitor id"));
634 }
635 Ok(CompositeExpr::Ref(MonitorRef { id: id.to_string() }))
636 }
637
638 fn parse_condition(&mut self) -> Result<Condition, ParseError> {
641 let operator = self.parse_cmp_op()?;
642 let critical = Scalar::new(self.cursor.take_number()?);
643 Ok(Condition {
644 operator,
645 critical,
646 critical_recovery: None,
647 warning: None,
648 warning_recovery: None,
649 })
650 }
651
652 fn parse_cmp_op(&mut self) -> Result<CmpOp, ParseError> {
653 for op in [
654 CmpOp::Ge,
655 CmpOp::Le,
656 CmpOp::Eq,
657 CmpOp::Ne,
658 CmpOp::Gt,
659 CmpOp::Lt,
660 ] {
661 if self.cursor.eat(op.as_token()) {
662 return Ok(op);
663 }
664 }
665 Err(self.cursor.error("expected a comparison operator"))
666 }
667
668 fn parse_string_args(&mut self) -> Result<Vec<String>, ParseError> {
671 let mut out = Vec::new();
672 if self.cursor.eat(")") {
673 return Ok(out);
674 }
675 loop {
676 self.cursor.skip_ws();
677 let value = match self.cursor.peek() {
678 Some('\'' | '"') => self.cursor.take_quoted()?,
679 _ => self
680 .cursor
681 .take_while(|c| c != ',' && c != ')')
682 .trim()
683 .to_string(),
684 };
685 out.push(value);
686 if self.cursor.eat(",") {
687 continue;
688 }
689 self.cursor.expect(")")?;
690 break;
691 }
692 Ok(out)
693 }
694
695 fn try_take_percentile_prefix(&mut self) -> Option<String> {
702 let rest = self.cursor.rest().trim_start();
703 let body = rest.strip_prefix('p')?;
706 let bytes = body.as_bytes();
707 let mut len = 0;
708 while len < bytes.len() && bytes[len].is_ascii_digit() {
709 len += 1;
710 }
711 if len == 0 {
712 return None;
713 }
714 if bytes.get(len) == Some(&b'.') {
715 let mut frac = len + 1;
716 while frac < bytes.len() && bytes[frac].is_ascii_digit() {
717 frac += 1;
718 }
719 if frac > len + 1 {
720 len = frac;
721 }
722 }
723 if !body[len..].trim_start().starts_with(':') {
725 return None;
726 }
727 let rank = body[..len].to_string();
728 self.cursor.eat("p");
730 self.cursor.take_while(|c| c.is_ascii_digit() || c == '.');
731 self.cursor.eat(":");
732 Some(rank)
733 }
734
735 fn take_ident(&mut self) -> Result<String, ParseError> {
736 let id = self.cursor.take_while(is_ident);
737 if id.is_empty() {
738 Err(self.cursor.error("expected an identifier"))
739 } else {
740 Ok(id.to_string())
741 }
742 }
743
744 fn take_window(&mut self) -> Result<Window, ParseError> {
745 let raw = self.cursor.take_while(is_ident);
746 Window::parse(raw).ok_or_else(|| self.cursor.error(format!("invalid window `{raw}`")))
747 }
748
749 fn take_metric_name(&mut self) -> Result<String, ParseError> {
751 let first = self.cursor.take_while(is_ident);
752 if first.is_empty() {
753 return Err(self.cursor.error("expected a metric name"));
754 }
755 self.continue_metric_name(first.to_string())
756 }
757
758 fn continue_metric_name(&mut self, first: String) -> Result<String, ParseError> {
761 let mut name = first;
762 loop {
763 if !self.cursor.rest().starts_with('.') {
764 break;
765 }
766 let mut probe = self.cursor.clone();
768 probe.eat(".");
769 let seg = probe.take_while(is_ident);
770 if seg.is_empty() || probe.rest().starts_with('(') {
771 break;
772 }
773 self.cursor.eat(".");
774 let committed = self.cursor.take_while(is_ident);
775 name.push('.');
776 name.push_str(committed);
777 }
778 Ok(name)
779 }
780}
781
782fn binding_power(op: ArithOp) -> (u8, u8) {
784 match op {
785 ArithOp::Add | ArithOp::Sub => (1, 2),
786 ArithOp::Mul | ArithOp::Div => (3, 4),
787 }
788}
789
790fn classify_filter(token: &str) -> Filter {
792 if token == "*" {
793 return Filter::All;
794 }
795 let (negated, body) = match token.strip_prefix('!') {
796 Some(rest) => (true, rest),
797 None => (false, token),
798 };
799 match body.split_once(':') {
800 Some((key, value)) => Filter::Tag {
801 negated,
802 key: key.to_string(),
803 value: value.to_string(),
804 },
805 None => Filter::Glob(token.to_string()),
806 }
807}
808
809fn bareword_value(word: &str) -> ParamValue {
811 match word {
812 "true" => ParamValue::Bool(true),
813 "false" => ParamValue::Bool(false),
814 other => ParamValue::Str(other.to_string()),
815 }
816}
817
818fn checked_u32(value: f64) -> Option<u32> {
820 if value.is_finite() && value.fract() == 0.0 && (0.0..=f64::from(u32::MAX)).contains(&value) {
821 Some(value as u32)
822 } else {
823 None
824 }
825}