Skip to content

Commit

Permalink
Implement span exporting limits (#45)
Browse files Browse the repository at this point in the history
* Implement span exporting limits

* license

* Add demo usage to sample app
  • Loading branch information
Mateusz Rzeszutek authored Jun 22, 2021
1 parent c295bb6 commit c7aaf14
Show file tree
Hide file tree
Showing 6 changed files with 464 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,31 @@

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.navigation.fragment.NavHostFragment;

import com.splunk.android.sample.databinding.FragmentSecondBinding;
import com.splunk.rum.SplunkRum;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;

public class SecondFragment extends Fragment {

private final ScheduledExecutorService spammer = Executors.newSingleThreadScheduledExecutor();
private final MutableLiveData<String> spanCountLabel = new MutableLiveData<>();
private final AtomicLong spans = new AtomicLong(0);

private ScheduledFuture<?> spamTask;

private FragmentSecondBinding binding;
private Tracer sampleAppTracer;

Expand All @@ -44,8 +58,13 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa
return binding.getRoot();
}

@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
binding.setLifecycleOwner(getViewLifecycleOwner());
binding.setSecondFragment(this);

resetLabel();

binding.buttonSecond.setOnClickListener(v -> {
//an example of using the OpenTelemetry API directly to generate a 100% custom span.
Expand All @@ -60,6 +79,7 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
span.end();
}
});
binding.buttonSpam.setOnClickListener(v -> toggleSpam());
}

@Override
Expand All @@ -68,4 +88,36 @@ public void onDestroyView() {
binding = null;
}

public LiveData<String> getSpanCountLabel() {
return spanCountLabel;
}

private void toggleSpam() {
if (spamTask == null) {
resetLabel();
spamTask = spammer.scheduleAtFixedRate(this::createSpamSpan, 0, 50, TimeUnit.MILLISECONDS);
binding.buttonSpam.setText(R.string.stop_spam);
} else {
spamTask.cancel(false);
spamTask = null;
binding.buttonSpam.setText(R.string.start_spam);
}
}

private void resetLabel() {
spans.set(0);
updateLabel();
}

private void updateLabel() {
spanCountLabel.postValue(getString(R.string.spam_status, spans.get()));
}

private void createSpamSpan() {
sampleAppTracer.spanBuilder("spam span no. " + spans.incrementAndGet())
.setAttribute("number", spans.get())
.startSpan()
.end();
updateLabel();
}
}
71 changes: 50 additions & 21 deletions sample-app/src/main/res/layout/fragment_second.xml
Original file line number Diff line number Diff line change
@@ -1,27 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondFragment">

<TextView
android:id="@+id/textview_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/button_second"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<data>
<variable
name="secondFragment"
type="com.splunk.android.sample.SecondFragment" />
</data>

<Button
android:id="@+id/button_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/previous"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview_second" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textview_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/button_second"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/previous"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview_second" />

<Button
android:id="@+id/button_spam"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/start_spam"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_second" />

<TextView
android:id="@+id/spam_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{secondFragment.spanCountLabel}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_spam" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
4 changes: 4 additions & 0 deletions sample-app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@
<string name="http_me_up">HTTP me up!</string>
<string name="http_me_bad">Http Error</string>
<string name="http_not_found">HTTP Not Found</string>

<string name="start_spam">Spam start!</string>
<string name="stop_spam">Spam stop</string>
<string name="spam_status">Created %d spans</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import com.splunk.android.rum.R;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -143,7 +144,13 @@ SpanExporter buildExporter(ConnectionUtil connectionUtil) {
// we'll do our best to hang on to the spans with the wrapping BufferingExporter.
ZipkinSpanExporter.baseLogger.setLevel(Level.SEVERE);
}
return new BufferingExporter(connectionUtil, ZipkinSpanExporter.builder().setEndpoint(endpoint).build());
ZipkinSpanExporter zipkinSpanExporter = ZipkinSpanExporter.builder().setEndpoint(endpoint).build();
SpanExporter throttlingExporter = ThrottlingExporter.newBuilder(zipkinSpanExporter)
.categorizeByAttribute(SplunkRum.COMPONENT_KEY)
.maxSpansInWindow(100)
.windowSize(Duration.ofSeconds(30))
.build();
return new BufferingExporter(connectionUtil, throttlingExporter);
}

static class InitializationEvent {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright Splunk Inc.
*
* 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.
*/

package com.splunk.rum;

import android.util.Log;

import java.time.Duration;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.export.SpanExporter;

class ThrottlingExporter implements SpanExporter {
private final SpanExporter delegate;
private final Function<SpanData, String> categoryFunction;
private final long windowSizeInNanos;
private final int maxSpansInWindow;
// note: no need to make this thread-safe since it will only ever be called from the BatchSpanProcessor worker thread.
private final Map<String, Window> categoryToWindow = new HashMap<>();

private ThrottlingExporter(Builder builder) {
this.delegate = builder.delegate;
this.categoryFunction = builder.categoryFunction;
this.windowSizeInNanos = builder.windowSize.toNanos();
this.maxSpansInWindow = builder.maxSpansInWindow;
}

static Builder newBuilder(SpanExporter delegate) {
return new Builder(delegate);
}

@Override
public CompletableResultCode export(Collection<SpanData> spans) {
List<SpanData> spansBelowLimit = new ArrayList<>();
for (SpanData span : spans) {
String category = categoryFunction.apply(span);
Window window = categoryToWindow.computeIfAbsent(category, k -> new Window());
if (!window.aboveLimit(span)) {
spansBelowLimit.add(span);
}
}
int dropped = spans.size() - spansBelowLimit.size();
if (dropped > 0) {
Log.d(SplunkRum.LOG_TAG, "Dropped " + dropped + " spans because of throttling");
}
return delegate.export(spansBelowLimit);
}

@Override
public CompletableResultCode flush() {
return delegate.flush();
}

@Override
public CompletableResultCode shutdown() {
return delegate.shutdown();
}

class Window {
private final Deque<Long> timestamps = new ArrayDeque<>();

// this function assumes that spans are always sorted by their end time (ascending)
boolean aboveLimit(SpanData spanData) {
long endNanos = spanData.getEndEpochNanos();
timestamps.addLast(endNanos);

// remove oldest entries until the window shrinks to the configured size
while (true) {
long first = timestamps.peekFirst();
if (endNanos - first < windowSizeInNanos) {
break;
}
timestamps.removeFirst();
}

boolean aboveLimit = timestamps.size() > maxSpansInWindow;
// don't count spans that were throttled
if (aboveLimit) {
timestamps.removeLast();
}
return aboveLimit;
}
}

static class Builder {
final SpanExporter delegate;
Function<SpanData, String> categoryFunction = span -> "default";
Duration windowSize = Duration.ofSeconds(30);
int maxSpansInWindow = 100;

private Builder(SpanExporter delegate) {
this.delegate = delegate;
}

Builder categorizeByAttribute(AttributeKey<String> attributeKey) {
categoryFunction = spanData -> spanData.getAttributes().get(attributeKey);
return this;
}

Builder windowSize(Duration timeWindow) {
this.windowSize = timeWindow;
return this;
}

Builder maxSpansInWindow(int maxSpansInWindow) {
this.maxSpansInWindow = maxSpansInWindow;
return this;
}

ThrottlingExporter build() {
return new ThrottlingExporter(this);
}
}
}
Loading

0 comments on commit c7aaf14

Please sign in to comment.