mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-02-03 13:11:53 +02:00
Unify the OTLP attribute transform (#2170)
* Unify the otlpmetric attribute transform * Unify attr conversion in otlptrace
This commit is contained in:
parent
a882ee3792
commit
1cb5cdca6b
@ -23,71 +23,79 @@ import (
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
)
|
||||
|
||||
// Attributes transforms a slice of KeyValues into a slice of OTLP attribute key-values.
|
||||
func Attributes(attrs []attribute.KeyValue) []*commonpb.KeyValue {
|
||||
// KeyValues transforms a slice of attribute KeyValues into OTLP key-values.
|
||||
func KeyValues(attrs []attribute.KeyValue) []*commonpb.KeyValue {
|
||||
if len(attrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]*commonpb.KeyValue, 0, len(attrs))
|
||||
for _, kv := range attrs {
|
||||
out = append(out, toAttribute(kv))
|
||||
out = append(out, KeyValue(kv))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ResourceAttributes transforms a Resource into a slice of OTLP attribute key-values.
|
||||
func ResourceAttributes(resource *resource.Resource) []*commonpb.KeyValue {
|
||||
if resource.Len() == 0 {
|
||||
// Iterator transforms an attribute iterator into OTLP key-values.
|
||||
func Iterator(iter attribute.Iterator) []*commonpb.KeyValue {
|
||||
l := iter.Len()
|
||||
if l == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]*commonpb.KeyValue, 0, resource.Len())
|
||||
for iter := resource.Iter(); iter.Next(); {
|
||||
out = append(out, toAttribute(iter.Attribute()))
|
||||
out := make([]*commonpb.KeyValue, 0, l)
|
||||
for iter.Next() {
|
||||
out = append(out, KeyValue(iter.Attribute()))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func toAttribute(v attribute.KeyValue) *commonpb.KeyValue {
|
||||
result := &commonpb.KeyValue{
|
||||
Key: string(v.Key),
|
||||
Value: new(commonpb.AnyValue),
|
||||
}
|
||||
switch v.Value.Type() {
|
||||
// ResourceAttributes transforms a Resource OTLP key-values.
|
||||
func ResourceAttributes(resource *resource.Resource) []*commonpb.KeyValue {
|
||||
return Iterator(resource.Iter())
|
||||
}
|
||||
|
||||
// KeyValue transforms an attribute KeyValue into an OTLP key-value.
|
||||
func KeyValue(kv attribute.KeyValue) *commonpb.KeyValue {
|
||||
return &commonpb.KeyValue{Key: string(kv.Key), Value: Value(kv.Value)}
|
||||
}
|
||||
|
||||
// Value transforms an attribute Value into an OTLP AnyValue.
|
||||
func Value(v attribute.Value) *commonpb.AnyValue {
|
||||
av := new(commonpb.AnyValue)
|
||||
switch v.Type() {
|
||||
case attribute.BOOL:
|
||||
result.Value.Value = &commonpb.AnyValue_BoolValue{
|
||||
BoolValue: v.Value.AsBool(),
|
||||
av.Value = &commonpb.AnyValue_BoolValue{
|
||||
BoolValue: v.AsBool(),
|
||||
}
|
||||
case attribute.INT64:
|
||||
result.Value.Value = &commonpb.AnyValue_IntValue{
|
||||
IntValue: v.Value.AsInt64(),
|
||||
av.Value = &commonpb.AnyValue_IntValue{
|
||||
IntValue: v.AsInt64(),
|
||||
}
|
||||
case attribute.FLOAT64:
|
||||
result.Value.Value = &commonpb.AnyValue_DoubleValue{
|
||||
DoubleValue: v.Value.AsFloat64(),
|
||||
av.Value = &commonpb.AnyValue_DoubleValue{
|
||||
DoubleValue: v.AsFloat64(),
|
||||
}
|
||||
case attribute.STRING:
|
||||
result.Value.Value = &commonpb.AnyValue_StringValue{
|
||||
StringValue: v.Value.AsString(),
|
||||
av.Value = &commonpb.AnyValue_StringValue{
|
||||
StringValue: v.AsString(),
|
||||
}
|
||||
case attribute.ARRAY:
|
||||
result.Value.Value = &commonpb.AnyValue_ArrayValue{
|
||||
av.Value = &commonpb.AnyValue_ArrayValue{
|
||||
ArrayValue: &commonpb.ArrayValue{
|
||||
Values: arrayValues(v),
|
||||
},
|
||||
}
|
||||
default:
|
||||
result.Value.Value = &commonpb.AnyValue_StringValue{
|
||||
av.Value = &commonpb.AnyValue_StringValue{
|
||||
StringValue: "INVALID",
|
||||
}
|
||||
}
|
||||
return result
|
||||
return av
|
||||
}
|
||||
|
||||
func arrayValues(kv attribute.KeyValue) []*commonpb.AnyValue {
|
||||
a := kv.Value.AsArray()
|
||||
func arrayValues(v attribute.Value) []*commonpb.AnyValue {
|
||||
a := v.AsArray()
|
||||
aType := reflect.TypeOf(a)
|
||||
var valueFunc func(reflect.Value) *commonpb.AnyValue
|
||||
switch aType.Elem().Kind() {
|
||||
|
@ -83,7 +83,7 @@ func TestAttributes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
} {
|
||||
got := Attributes(test.attrs)
|
||||
got := KeyValues(test.attrs)
|
||||
if !assert.Len(t, got, len(test.expected)) {
|
||||
continue
|
||||
}
|
||||
@ -142,7 +142,7 @@ func TestArrayAttributes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
} {
|
||||
actualArrayAttributes := Attributes(test.attrs)
|
||||
actualArrayAttributes := KeyValues(test.attrs)
|
||||
expectedArrayAttributes := test.expected
|
||||
if !assert.Len(t, actualArrayAttributes, len(expectedArrayAttributes)) {
|
||||
continue
|
||||
|
@ -318,7 +318,7 @@ func gaugeArray(record export.Record, points []aggregation.Point) (*metricpb.Met
|
||||
Unit: string(desc.Unit()),
|
||||
}
|
||||
|
||||
pbAttrs := keyValues(labels.Iter())
|
||||
pbAttrs := Iterator(labels.Iter())
|
||||
|
||||
ndp := make([]*metricpb.NumberDataPoint, 0, len(points))
|
||||
switch nk := desc.NumberKind(); nk {
|
||||
@ -375,7 +375,7 @@ func gaugePoint(record export.Record, num number.Number, start, end time.Time) (
|
||||
Value: &metricpb.NumberDataPoint_AsInt{
|
||||
AsInt: num.CoerceToInt64(n),
|
||||
},
|
||||
Attributes: keyValues(labels.Iter()),
|
||||
Attributes: Iterator(labels.Iter()),
|
||||
StartTimeUnixNano: toNanos(start),
|
||||
TimeUnixNano: toNanos(end),
|
||||
},
|
||||
@ -390,7 +390,7 @@ func gaugePoint(record export.Record, num number.Number, start, end time.Time) (
|
||||
Value: &metricpb.NumberDataPoint_AsDouble{
|
||||
AsDouble: num.CoerceToFloat64(n),
|
||||
},
|
||||
Attributes: keyValues(labels.Iter()),
|
||||
Attributes: Iterator(labels.Iter()),
|
||||
StartTimeUnixNano: toNanos(start),
|
||||
TimeUnixNano: toNanos(end),
|
||||
},
|
||||
@ -435,7 +435,7 @@ func sumPoint(record export.Record, num number.Number, start, end time.Time, ek
|
||||
Value: &metricpb.NumberDataPoint_AsInt{
|
||||
AsInt: num.CoerceToInt64(n),
|
||||
},
|
||||
Attributes: keyValues(labels.Iter()),
|
||||
Attributes: Iterator(labels.Iter()),
|
||||
StartTimeUnixNano: toNanos(start),
|
||||
TimeUnixNano: toNanos(end),
|
||||
},
|
||||
@ -452,7 +452,7 @@ func sumPoint(record export.Record, num number.Number, start, end time.Time, ek
|
||||
Value: &metricpb.NumberDataPoint_AsDouble{
|
||||
AsDouble: num.CoerceToFloat64(n),
|
||||
},
|
||||
Attributes: keyValues(labels.Iter()),
|
||||
Attributes: Iterator(labels.Iter()),
|
||||
StartTimeUnixNano: toNanos(start),
|
||||
TimeUnixNano: toNanos(end),
|
||||
},
|
||||
@ -502,7 +502,7 @@ func minMaxSumCount(record export.Record, a aggregation.MinMaxSumCount) (*metric
|
||||
DataPoints: []*metricpb.SummaryDataPoint{
|
||||
{
|
||||
Sum: sum.CoerceToFloat64(desc.NumberKind()),
|
||||
Attributes: keyValues(labels.Iter()),
|
||||
Attributes: Iterator(labels.Iter()),
|
||||
StartTimeUnixNano: toNanos(record.StartTime()),
|
||||
TimeUnixNano: toNanos(record.EndTime()),
|
||||
Count: uint64(count),
|
||||
@ -566,7 +566,7 @@ func histogramPoint(record export.Record, ek export.ExportKind, a aggregation.Hi
|
||||
DataPoints: []*metricpb.HistogramDataPoint{
|
||||
{
|
||||
Sum: sum.CoerceToFloat64(desc.NumberKind()),
|
||||
Attributes: keyValues(labels.Iter()),
|
||||
Attributes: Iterator(labels.Iter()),
|
||||
StartTimeUnixNano: toNanos(record.StartTime()),
|
||||
TimeUnixNano: toNanos(record.EndTime()),
|
||||
Count: uint64(count),
|
||||
@ -579,112 +579,3 @@ func histogramPoint(record export.Record, ek export.ExportKind, a aggregation.Hi
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// keyValues transforms an attribute iterator into an OTLP KeyValues.
|
||||
func keyValues(iter attribute.Iterator) []*commonpb.KeyValue {
|
||||
l := iter.Len()
|
||||
if l == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]*commonpb.KeyValue, 0, l)
|
||||
for iter.Next() {
|
||||
kv := iter.Label()
|
||||
result = append(result, &commonpb.KeyValue{
|
||||
Key: string(kv.Key),
|
||||
Value: value(kv.Value),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// value transforms an attribute Value into an OTLP AnyValue.
|
||||
func value(v attribute.Value) *commonpb.AnyValue {
|
||||
switch v.Type() {
|
||||
case attribute.BOOL:
|
||||
return &commonpb.AnyValue{
|
||||
Value: &commonpb.AnyValue_BoolValue{
|
||||
BoolValue: v.AsBool(),
|
||||
},
|
||||
}
|
||||
case attribute.INT64:
|
||||
return &commonpb.AnyValue{
|
||||
Value: &commonpb.AnyValue_IntValue{
|
||||
IntValue: v.AsInt64(),
|
||||
},
|
||||
}
|
||||
case attribute.FLOAT64:
|
||||
return &commonpb.AnyValue{
|
||||
Value: &commonpb.AnyValue_DoubleValue{
|
||||
DoubleValue: v.AsFloat64(),
|
||||
},
|
||||
}
|
||||
case attribute.ARRAY:
|
||||
return &commonpb.AnyValue{
|
||||
Value: &commonpb.AnyValue_ArrayValue{
|
||||
ArrayValue: &commonpb.ArrayValue{
|
||||
Values: arrayValue(v.AsArray()),
|
||||
},
|
||||
},
|
||||
}
|
||||
default:
|
||||
return &commonpb.AnyValue{
|
||||
Value: &commonpb.AnyValue_StringValue{
|
||||
StringValue: v.Emit(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// arrayValue transforms an attribute Value of ARRAY type into an slice of
|
||||
// OTLP AnyValue.
|
||||
func arrayValue(arr interface{}) []*commonpb.AnyValue {
|
||||
var av []*commonpb.AnyValue
|
||||
switch val := arr.(type) {
|
||||
case []bool:
|
||||
av = make([]*commonpb.AnyValue, len(val))
|
||||
for i, v := range val {
|
||||
av[i] = &commonpb.AnyValue{
|
||||
Value: &commonpb.AnyValue_BoolValue{
|
||||
BoolValue: v,
|
||||
},
|
||||
}
|
||||
}
|
||||
case []int:
|
||||
av = make([]*commonpb.AnyValue, len(val))
|
||||
for i, v := range val {
|
||||
av[i] = &commonpb.AnyValue{
|
||||
Value: &commonpb.AnyValue_IntValue{
|
||||
IntValue: int64(v),
|
||||
},
|
||||
}
|
||||
}
|
||||
case []int64:
|
||||
av = make([]*commonpb.AnyValue, len(val))
|
||||
for i, v := range val {
|
||||
av[i] = &commonpb.AnyValue{
|
||||
Value: &commonpb.AnyValue_IntValue{
|
||||
IntValue: v,
|
||||
},
|
||||
}
|
||||
}
|
||||
case []float64:
|
||||
av = make([]*commonpb.AnyValue, len(val))
|
||||
for i, v := range val {
|
||||
av[i] = &commonpb.AnyValue{
|
||||
Value: &commonpb.AnyValue_DoubleValue{
|
||||
DoubleValue: v,
|
||||
},
|
||||
}
|
||||
}
|
||||
case []string:
|
||||
av = make([]*commonpb.AnyValue, len(val))
|
||||
for i, v := range val {
|
||||
av[i] = &commonpb.AnyValue{
|
||||
Value: &commonpb.AnyValue_StringValue{
|
||||
StringValue: v,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return av
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ func TestStringKeyValues(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
labels := attribute.NewSet(test.kvs...)
|
||||
assert.Equal(t, test.expected, keyValues(labels.Iter()))
|
||||
assert.Equal(t, test.expected, Iterator(labels.Iter()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,5 +44,5 @@ func TestResourceAttributes(t *testing.T) {
|
||||
if !assert.Len(t, attrs, 2) {
|
||||
return
|
||||
}
|
||||
assert.ElementsMatch(t, Attributes(attrs), got)
|
||||
assert.ElementsMatch(t, KeyValues(attrs), got)
|
||||
}
|
||||
|
@ -23,71 +23,79 @@ import (
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
)
|
||||
|
||||
// Attributes transforms a slice of KeyValues into a slice of OTLP attribute key-values.
|
||||
func Attributes(attrs []attribute.KeyValue) []*commonpb.KeyValue {
|
||||
// KeyValues transforms a slice of attribute KeyValues into OTLP key-values.
|
||||
func KeyValues(attrs []attribute.KeyValue) []*commonpb.KeyValue {
|
||||
if len(attrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]*commonpb.KeyValue, 0, len(attrs))
|
||||
for _, kv := range attrs {
|
||||
out = append(out, toAttribute(kv))
|
||||
out = append(out, KeyValue(kv))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ResourceAttributes transforms a Resource into a slice of OTLP attribute key-values.
|
||||
func ResourceAttributes(resource *resource.Resource) []*commonpb.KeyValue {
|
||||
if resource.Len() == 0 {
|
||||
// Iterator transforms an attribute iterator into OTLP key-values.
|
||||
func Iterator(iter attribute.Iterator) []*commonpb.KeyValue {
|
||||
l := iter.Len()
|
||||
if l == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]*commonpb.KeyValue, 0, resource.Len())
|
||||
for iter := resource.Iter(); iter.Next(); {
|
||||
out = append(out, toAttribute(iter.Attribute()))
|
||||
out := make([]*commonpb.KeyValue, 0, l)
|
||||
for iter.Next() {
|
||||
out = append(out, KeyValue(iter.Attribute()))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func toAttribute(v attribute.KeyValue) *commonpb.KeyValue {
|
||||
result := &commonpb.KeyValue{
|
||||
Key: string(v.Key),
|
||||
Value: new(commonpb.AnyValue),
|
||||
}
|
||||
switch v.Value.Type() {
|
||||
// ResourceAttributes transforms a Resource OTLP key-values.
|
||||
func ResourceAttributes(resource *resource.Resource) []*commonpb.KeyValue {
|
||||
return Iterator(resource.Iter())
|
||||
}
|
||||
|
||||
// KeyValue transforms an attribute KeyValue into an OTLP key-value.
|
||||
func KeyValue(kv attribute.KeyValue) *commonpb.KeyValue {
|
||||
return &commonpb.KeyValue{Key: string(kv.Key), Value: Value(kv.Value)}
|
||||
}
|
||||
|
||||
// Value transforms an attribute Value into an OTLP AnyValue.
|
||||
func Value(v attribute.Value) *commonpb.AnyValue {
|
||||
av := new(commonpb.AnyValue)
|
||||
switch v.Type() {
|
||||
case attribute.BOOL:
|
||||
result.Value.Value = &commonpb.AnyValue_BoolValue{
|
||||
BoolValue: v.Value.AsBool(),
|
||||
av.Value = &commonpb.AnyValue_BoolValue{
|
||||
BoolValue: v.AsBool(),
|
||||
}
|
||||
case attribute.INT64:
|
||||
result.Value.Value = &commonpb.AnyValue_IntValue{
|
||||
IntValue: v.Value.AsInt64(),
|
||||
av.Value = &commonpb.AnyValue_IntValue{
|
||||
IntValue: v.AsInt64(),
|
||||
}
|
||||
case attribute.FLOAT64:
|
||||
result.Value.Value = &commonpb.AnyValue_DoubleValue{
|
||||
DoubleValue: v.Value.AsFloat64(),
|
||||
av.Value = &commonpb.AnyValue_DoubleValue{
|
||||
DoubleValue: v.AsFloat64(),
|
||||
}
|
||||
case attribute.STRING:
|
||||
result.Value.Value = &commonpb.AnyValue_StringValue{
|
||||
StringValue: v.Value.AsString(),
|
||||
av.Value = &commonpb.AnyValue_StringValue{
|
||||
StringValue: v.AsString(),
|
||||
}
|
||||
case attribute.ARRAY:
|
||||
result.Value.Value = &commonpb.AnyValue_ArrayValue{
|
||||
av.Value = &commonpb.AnyValue_ArrayValue{
|
||||
ArrayValue: &commonpb.ArrayValue{
|
||||
Values: arrayValues(v),
|
||||
},
|
||||
}
|
||||
default:
|
||||
result.Value.Value = &commonpb.AnyValue_StringValue{
|
||||
av.Value = &commonpb.AnyValue_StringValue{
|
||||
StringValue: "INVALID",
|
||||
}
|
||||
}
|
||||
return result
|
||||
return av
|
||||
}
|
||||
|
||||
func arrayValues(kv attribute.KeyValue) []*commonpb.AnyValue {
|
||||
a := kv.Value.AsArray()
|
||||
func arrayValues(v attribute.Value) []*commonpb.AnyValue {
|
||||
a := v.AsArray()
|
||||
aType := reflect.TypeOf(a)
|
||||
var valueFunc func(reflect.Value) *commonpb.AnyValue
|
||||
switch aType.Elem().Kind() {
|
||||
|
@ -83,7 +83,7 @@ func TestAttributes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
} {
|
||||
got := Attributes(test.attrs)
|
||||
got := KeyValues(test.attrs)
|
||||
if !assert.Len(t, got, len(test.expected)) {
|
||||
continue
|
||||
}
|
||||
@ -142,7 +142,7 @@ func TestArrayAttributes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
} {
|
||||
actualArrayAttributes := Attributes(test.attrs)
|
||||
actualArrayAttributes := KeyValues(test.attrs)
|
||||
expectedArrayAttributes := test.expected
|
||||
if !assert.Len(t, actualArrayAttributes, len(expectedArrayAttributes)) {
|
||||
continue
|
||||
|
@ -44,5 +44,5 @@ func TestResourceAttributes(t *testing.T) {
|
||||
if !assert.Len(t, attrs, 2) {
|
||||
return
|
||||
}
|
||||
assert.ElementsMatch(t, Attributes(attrs), got)
|
||||
assert.ElementsMatch(t, KeyValues(attrs), got)
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ func span(sd tracesdk.ReadOnlySpan) *tracepb.Span {
|
||||
Links: links(sd.Links()),
|
||||
Kind: spanKind(sd.SpanKind()),
|
||||
Name: sd.Name(),
|
||||
Attributes: Attributes(sd.Attributes()),
|
||||
Attributes: KeyValues(sd.Attributes()),
|
||||
Events: spanEvents(sd.Events()),
|
||||
DroppedAttributesCount: uint32(sd.DroppedAttributes()),
|
||||
DroppedEventsCount: uint32(sd.DroppedEvents()),
|
||||
@ -163,7 +163,7 @@ func links(links []tracesdk.Link) []*tracepb.Span_Link {
|
||||
sl = append(sl, &tracepb.Span_Link{
|
||||
TraceId: tid[:],
|
||||
SpanId: sid[:],
|
||||
Attributes: Attributes(otLink.Attributes),
|
||||
Attributes: KeyValues(otLink.Attributes),
|
||||
})
|
||||
}
|
||||
return sl
|
||||
@ -192,7 +192,7 @@ func spanEvents(es []tracesdk.Event) []*tracepb.Span_Event {
|
||||
&tracepb.Span_Event{
|
||||
Name: e.Name,
|
||||
TimeUnixNano: uint64(e.Time.UnixNano()),
|
||||
Attributes: Attributes(e.Attributes),
|
||||
Attributes: KeyValues(e.Attributes),
|
||||
// TODO (rghetia) : Add Drop Counts when supported.
|
||||
},
|
||||
)
|
||||
|
@ -98,7 +98,7 @@ func TestSpanEvent(t *testing.T) {
|
||||
eventTimestamp := uint64(1589932800 * 1e9)
|
||||
assert.Equal(t, &tracepb.Span_Event{Name: "test 1", Attributes: nil, TimeUnixNano: eventTimestamp}, got[0])
|
||||
// Do not test Attributes directly, just that the return value goes to the correct field.
|
||||
assert.Equal(t, &tracepb.Span_Event{Name: "test 2", Attributes: Attributes(attrs), TimeUnixNano: eventTimestamp}, got[1])
|
||||
assert.Equal(t, &tracepb.Span_Event{Name: "test 2", Attributes: KeyValues(attrs), TimeUnixNano: eventTimestamp}, got[1])
|
||||
}
|
||||
|
||||
func TestExcessiveSpanEvents(t *testing.T) {
|
||||
@ -145,7 +145,7 @@ func TestLinks(t *testing.T) {
|
||||
assert.Equal(t, expected, got[0])
|
||||
|
||||
// Do not test Attributes directly, just that the return value goes to the correct field.
|
||||
expected.Attributes = Attributes(attrs)
|
||||
expected.Attributes = KeyValues(attrs)
|
||||
assert.Equal(t, expected, got[1])
|
||||
|
||||
// Changes to our links should not change the produced links.
|
||||
@ -288,7 +288,7 @@ func TestSpanData(t *testing.T) {
|
||||
Status: status(spanData.Status.Code, spanData.Status.Description),
|
||||
Events: spanEvents(spanData.Events),
|
||||
Links: links(spanData.Links),
|
||||
Attributes: Attributes(spanData.Attributes),
|
||||
Attributes: KeyValues(spanData.Attributes),
|
||||
DroppedAttributesCount: 1,
|
||||
DroppedEventsCount: 2,
|
||||
DroppedLinksCount: 3,
|
||||
|
Loading…
x
Reference in New Issue
Block a user