| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | // Copyright 2024 The Prometheus Authors
 | 
					
						
							|  |  |  | // Licensed under the Apache License, Version 2.0 (the "License");
 | 
					
						
							|  |  |  | // you may not use this file except in compliance with the License.
 | 
					
						
							|  |  |  | // You may obtain a copy of the License at
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // http://www.apache.org/licenses/LICENSE-2.0
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // Unless required by applicable law or agreed to in writing, software
 | 
					
						
							|  |  |  | // distributed under the License is distributed on an "AS IS" BASIS,
 | 
					
						
							|  |  |  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
					
						
							|  |  |  | // See the License for the specific language governing permissions and
 | 
					
						
							|  |  |  | // limitations under the License.
 | 
					
						
							|  |  |  | // Provenance-includes-location: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/95e8f8fdc2a9dc87230406c9a3cf02be4fd68bea/pkg/translator/prometheusremotewrite/metrics_to_prw.go
 | 
					
						
							|  |  |  | // Provenance-includes-license: Apache-2.0
 | 
					
						
							|  |  |  | // Provenance-includes-copyright: Copyright The OpenTelemetry Authors.
 | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | package prometheusremotewrite | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 	"context" | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 	"errors" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 	"sort" | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	"go.opentelemetry.io/collector/pdata/pcommon" | 
					
						
							|  |  |  | 	"go.opentelemetry.io/collector/pdata/pmetric" | 
					
						
							|  |  |  | 	"go.uber.org/multierr" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 	"github.com/prometheus/prometheus/prompb" | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 	prometheustranslator "github.com/prometheus/prometheus/storage/remote/otlptranslator/prometheus" | 
					
						
							| 
									
										
										
										
											2024-08-21 21:22:38 +08:00
										 |  |  | 	"github.com/prometheus/prometheus/util/annotations" | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type Settings struct { | 
					
						
							| 
									
										
										
										
											2024-11-29 22:19:14 +08:00
										 |  |  | 	Namespace                         string | 
					
						
							|  |  |  | 	ExternalLabels                    map[string]string | 
					
						
							|  |  |  | 	DisableTargetInfo                 bool | 
					
						
							|  |  |  | 	ExportCreatedMetric               bool | 
					
						
							|  |  |  | 	AddMetricSuffixes                 bool | 
					
						
							|  |  |  | 	AllowUTF8                         bool | 
					
						
							|  |  |  | 	PromoteResourceAttributes         []string | 
					
						
							|  |  |  | 	KeepIdentifyingResourceAttributes bool | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-01 18:37:58 +08:00
										 |  |  | // PrometheusConverter converts from OTel write format to Prometheus remote write format.
 | 
					
						
							| 
									
										
										
										
											2024-04-30 17:37:00 +08:00
										 |  |  | type PrometheusConverter struct { | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 	unique    map[uint64]*prompb.TimeSeries | 
					
						
							|  |  |  | 	conflicts map[uint64][]*prompb.TimeSeries | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 	everyN    everyNTimes | 
					
						
							| 
									
										
										
										
											2024-11-20 18:13:03 +08:00
										 |  |  | 	metadata  []prompb.MetricMetadata | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-30 17:37:00 +08:00
										 |  |  | func NewPrometheusConverter() *PrometheusConverter { | 
					
						
							|  |  |  | 	return &PrometheusConverter{ | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 		unique:    map[uint64]*prompb.TimeSeries{}, | 
					
						
							|  |  |  | 		conflicts: map[uint64][]*prompb.TimeSeries{}, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-30 17:37:00 +08:00
										 |  |  | // FromMetrics converts pmetric.Metrics to Prometheus remote write format.
 | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | func (c *PrometheusConverter) FromMetrics(ctx context.Context, md pmetric.Metrics, settings Settings) (annots annotations.Annotations, errs error) { | 
					
						
							|  |  |  | 	c.everyN = everyNTimes{n: 128} | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 	resourceMetricsSlice := md.ResourceMetrics() | 
					
						
							| 
									
										
										
										
											2024-11-20 18:13:03 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	numMetrics := 0 | 
					
						
							|  |  |  | 	for i := 0; i < resourceMetricsSlice.Len(); i++ { | 
					
						
							|  |  |  | 		scopeMetricsSlice := resourceMetricsSlice.At(i).ScopeMetrics() | 
					
						
							|  |  |  | 		for j := 0; j < scopeMetricsSlice.Len(); j++ { | 
					
						
							|  |  |  | 			numMetrics += scopeMetricsSlice.At(j).Metrics().Len() | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	c.metadata = make([]prompb.MetricMetadata, 0, numMetrics) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 	for i := 0; i < resourceMetricsSlice.Len(); i++ { | 
					
						
							|  |  |  | 		resourceMetrics := resourceMetricsSlice.At(i) | 
					
						
							|  |  |  | 		resource := resourceMetrics.Resource() | 
					
						
							|  |  |  | 		scopeMetricsSlice := resourceMetrics.ScopeMetrics() | 
					
						
							|  |  |  | 		// keep track of the most recent timestamp in the ResourceMetrics for
 | 
					
						
							|  |  |  | 		// use with the "target" info metric
 | 
					
						
							|  |  |  | 		var mostRecentTimestamp pcommon.Timestamp | 
					
						
							|  |  |  | 		for j := 0; j < scopeMetricsSlice.Len(); j++ { | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 			metricSlice := scopeMetricsSlice.At(j).Metrics() | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			// TODO: decide if instrumentation library information should be exported as labels
 | 
					
						
							|  |  |  | 			for k := 0; k < metricSlice.Len(); k++ { | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 				if err := c.everyN.checkContext(ctx); err != nil { | 
					
						
							|  |  |  | 					errs = multierr.Append(errs, err) | 
					
						
							|  |  |  | 					return | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 				metric := metricSlice.At(k) | 
					
						
							| 
									
										
										
										
											2024-04-30 09:45:20 +08:00
										 |  |  | 				mostRecentTimestamp = max(mostRecentTimestamp, mostRecentTimestampInMetric(metric)) | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 				if !isValidAggregationTemporality(metric) { | 
					
						
							|  |  |  | 					errs = multierr.Append(errs, fmt.Errorf("invalid temporality and type combination for metric %q", metric.Name())) | 
					
						
							|  |  |  | 					continue | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-06 22:30:39 +08:00
										 |  |  | 				var promName string | 
					
						
							|  |  |  | 				if settings.AllowUTF8 { | 
					
						
							|  |  |  | 					promName = prometheustranslator.BuildMetricName(metric, settings.Namespace, settings.AddMetricSuffixes) | 
					
						
							|  |  |  | 				} else { | 
					
						
							|  |  |  | 					promName = prometheustranslator.BuildCompliantMetricName(metric, settings.Namespace, settings.AddMetricSuffixes) | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2024-11-20 18:13:03 +08:00
										 |  |  | 				c.metadata = append(c.metadata, prompb.MetricMetadata{ | 
					
						
							|  |  |  | 					Type:             otelMetricTypeToPromMetricType(metric), | 
					
						
							|  |  |  | 					MetricFamilyName: promName, | 
					
						
							|  |  |  | 					Help:             metric.Description(), | 
					
						
							|  |  |  | 					Unit:             metric.Unit(), | 
					
						
							|  |  |  | 				}) | 
					
						
							| 
									
										
										
										
											2024-02-22 16:09:41 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 				// handle individual metrics based on type
 | 
					
						
							| 
									
										
										
										
											2023-11-15 22:09:15 +08:00
										 |  |  | 				//exhaustive:enforce
 | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 				switch metric.Type() { | 
					
						
							|  |  |  | 				case pmetric.MetricTypeGauge: | 
					
						
							|  |  |  | 					dataPoints := metric.Gauge().DataPoints() | 
					
						
							|  |  |  | 					if dataPoints.Len() == 0 { | 
					
						
							|  |  |  | 						errs = multierr.Append(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name())) | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 						break | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 					} | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 					if err := c.addGaugeNumberDataPoints(ctx, dataPoints, resource, settings, promName); err != nil { | 
					
						
							|  |  |  | 						errs = multierr.Append(errs, err) | 
					
						
							|  |  |  | 						if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { | 
					
						
							|  |  |  | 							return | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 					} | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 				case pmetric.MetricTypeSum: | 
					
						
							|  |  |  | 					dataPoints := metric.Sum().DataPoints() | 
					
						
							|  |  |  | 					if dataPoints.Len() == 0 { | 
					
						
							|  |  |  | 						errs = multierr.Append(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name())) | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 						break | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 					} | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 					if err := c.addSumNumberDataPoints(ctx, dataPoints, resource, metric, settings, promName); err != nil { | 
					
						
							|  |  |  | 						errs = multierr.Append(errs, err) | 
					
						
							|  |  |  | 						if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { | 
					
						
							|  |  |  | 							return | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 					} | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 				case pmetric.MetricTypeHistogram: | 
					
						
							|  |  |  | 					dataPoints := metric.Histogram().DataPoints() | 
					
						
							|  |  |  | 					if dataPoints.Len() == 0 { | 
					
						
							|  |  |  | 						errs = multierr.Append(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name())) | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 						break | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 					} | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 					if err := c.addHistogramDataPoints(ctx, dataPoints, resource, settings, promName); err != nil { | 
					
						
							|  |  |  | 						errs = multierr.Append(errs, err) | 
					
						
							|  |  |  | 						if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { | 
					
						
							|  |  |  | 							return | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 					} | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 				case pmetric.MetricTypeExponentialHistogram: | 
					
						
							|  |  |  | 					dataPoints := metric.ExponentialHistogram().DataPoints() | 
					
						
							|  |  |  | 					if dataPoints.Len() == 0 { | 
					
						
							|  |  |  | 						errs = multierr.Append(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name())) | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 						break | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 					} | 
					
						
							| 
									
										
										
										
											2024-08-21 21:22:38 +08:00
										 |  |  | 					ws, err := c.addExponentialHistogramDataPoints( | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 						ctx, | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 						dataPoints, | 
					
						
							|  |  |  | 						resource, | 
					
						
							|  |  |  | 						settings, | 
					
						
							|  |  |  | 						promName, | 
					
						
							| 
									
										
										
										
											2024-08-21 21:22:38 +08:00
										 |  |  | 					) | 
					
						
							|  |  |  | 					annots.Merge(ws) | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 					if err != nil { | 
					
						
							|  |  |  | 						errs = multierr.Append(errs, err) | 
					
						
							|  |  |  | 						if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { | 
					
						
							|  |  |  | 							return | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 					} | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 				case pmetric.MetricTypeSummary: | 
					
						
							|  |  |  | 					dataPoints := metric.Summary().DataPoints() | 
					
						
							|  |  |  | 					if dataPoints.Len() == 0 { | 
					
						
							|  |  |  | 						errs = multierr.Append(errs, fmt.Errorf("empty data points. %s is dropped", metric.Name())) | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 						break | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 					} | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 					if err := c.addSummaryDataPoints(ctx, dataPoints, resource, settings, promName); err != nil { | 
					
						
							|  |  |  | 						errs = multierr.Append(errs, err) | 
					
						
							|  |  |  | 						if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { | 
					
						
							|  |  |  | 							return | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 					} | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 				default: | 
					
						
							|  |  |  | 					errs = multierr.Append(errs, errors.New("unsupported metric type")) | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 		addResourceTargetInfo(resource, settings, mostRecentTimestamp, c) | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-21 21:22:38 +08:00
										 |  |  | 	return annots, errs | 
					
						
							| 
									
										
										
										
											2023-07-28 18:35:28 +08:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | func isSameMetric(ts *prompb.TimeSeries, lbls []prompb.Label) bool { | 
					
						
							|  |  |  | 	if len(ts.Labels) != len(lbls) { | 
					
						
							|  |  |  | 		return false | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	for i, l := range ts.Labels { | 
					
						
							|  |  |  | 		if l.Name != ts.Labels[i].Name || l.Value != ts.Labels[i].Value { | 
					
						
							|  |  |  | 			return false | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return true | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // addExemplars adds exemplars for the dataPoint. For each exemplar, if it can find a bucket bound corresponding to its value,
 | 
					
						
							|  |  |  | // the exemplar is added to the bucket bound's time series, provided that the time series' has samples.
 | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | func (c *PrometheusConverter) addExemplars(ctx context.Context, dataPoint pmetric.HistogramDataPoint, bucketBounds []bucketBoundsData) error { | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 	if len(bucketBounds) == 0 { | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 		return nil | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 	exemplars, err := getPromExemplars(ctx, &c.everyN, dataPoint) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 	if len(exemplars) == 0 { | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 		return nil | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	sort.Sort(byBucketBoundsData(bucketBounds)) | 
					
						
							|  |  |  | 	for _, exemplar := range exemplars { | 
					
						
							|  |  |  | 		for _, bound := range bucketBounds { | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 			if err := c.everyN.checkContext(ctx); err != nil { | 
					
						
							|  |  |  | 				return err | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 			if len(bound.ts.Samples) > 0 && exemplar.Value <= bound.bound { | 
					
						
							|  |  |  | 				bound.ts.Exemplars = append(bound.ts.Exemplars, exemplar) | 
					
						
							|  |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-09-08 23:13:40 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // addSample finds a TimeSeries that corresponds to lbls, and adds sample to it.
 | 
					
						
							|  |  |  | // If there is no corresponding TimeSeries already, it's created.
 | 
					
						
							|  |  |  | // The corresponding TimeSeries is returned.
 | 
					
						
							|  |  |  | // If either lbls is nil/empty or sample is nil, nothing is done.
 | 
					
						
							| 
									
										
										
										
											2024-04-30 17:37:00 +08:00
										 |  |  | func (c *PrometheusConverter) addSample(sample *prompb.Sample, lbls []prompb.Label) *prompb.TimeSeries { | 
					
						
							| 
									
										
										
										
											2024-04-30 17:29:52 +08:00
										 |  |  | 	if sample == nil || len(lbls) == 0 { | 
					
						
							|  |  |  | 		// This shouldn't happen
 | 
					
						
							|  |  |  | 		return nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	ts, _ := c.getOrCreateTimeSeries(lbls) | 
					
						
							|  |  |  | 	ts.Samples = append(ts.Samples, *sample) | 
					
						
							|  |  |  | 	return ts | 
					
						
							|  |  |  | } |