Angular is a very opinionated front end framework that utilizes Typescript. It provides a ton of useful tools like a built in HttpClient library, routing, etc. Because it’s opinionated, dropping in javascript libraries can be challenging, unless you understand these opinions and their intentions.

We’re going to add traces in two approaches. The first being more of a drop in, the second a more directed and opinionated approach.

First Approach

Our first approach is going to be just following the lightstep javascript cookbook so that we get a quick win that traces all XHR/Http calls. We are going to utilize the xhr_instrumentation:true flag to trace all XHR calls and get free context propagation into our downstream services. Woo!

Before We Start

  • Sign up for a free LightStep Developer account to quickly visualize traces without any local setup required.
  • CD into sprint boot apps testapp-1 and testapp-2 and do gradle bootRun.
  • Test app 1, and then 2, in that order. (Make sure to update your AccessToken/ProjectToken found in TracingConfig.java.)
  • When you run ng serve on the front end — it will automatically hit localhost:8080 on page load, creating traces and sending them to LightStep.

Here We Go

  1. First add opentracing and the lightstep tracer to your project npm install --save lightstep-tracer opentracing
  2. Create a new service called Traver service that looks like below – ng g service tracer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse } from '@angular/common/http';
import * as lightstepTracer from 'lightstep-tracer';
import * as opentracing from 'opentracing';

@Injectable({
  providedIn: 'root'
})
export class TracerService {

  constructor() {
    // Put your Access/Project Token in your env config for prod
    this.initGlobalTracer('YOUR_ACCESS_TOKEN', 'Angular');
  }

  // Due to the xhr_instrumentation flag being true, all http calls will be traced
  initGlobalTracer(accessToken: string, componentName: string) {
    const options: lightstepTracer.TracerOptions = {
      access_token: accessToken,
      component_name: componentName,
      xhr_instrumentation: true
    };

    opentracing.initGlobalTracer( new lightstepTracer.Tracer(options));
  }
}

3. Inject this service into your AppComponent’s constructor so that you get out of the box instrumentation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Component } from '@angular/core';
import { TracerService } from './tracer.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'front-end';

  constructor(private trace: TracerService) {

  }
}

So what exactly is happening?

  1. The AppComponent is always loaded first (unless you’ve done something unconventional with your app). In this way we can be sure that regardless of what component we fetch data in, it will be traced.
  2. Our TracerServices’s contructor initializes our Open Tracing Global Tracer with the proper settings to send data into LightStep

Manual instrumentation

Again, by more or less following the cookbook, we can do something like this in any component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { Component, OnInit } from '@angular/core';
import { TracerService } from '../tracer.service';
import { Observable, Observer } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import * as opentracing from 'opentracing';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {

  result: any;
  url = 'http://localhost:8080';
  constructor(private http: HttpClient) {

    const span = opentracing.globalTracer().startSpan('Get:80');
    this.http.get(this.url).subscribe((data) => {
      this.result = data;
      span.log({response : this.result});
    },

    error => {
      this.result = error;
      span.setTag('error', true);
      span.log({data: this.result});
    },
    () => {
      span.finish();
    });
  }

  ngOnInit() {
  }

}

Second Approach: Creating an Interceptor

Tracing each and every API call can be a pain. While you might handle certain request differently than others, it is very likely that you desire a more framework level approach to tracing your Http Calls.

Implementing tracing as an HttpInterceptor is a far better method within Angular to handle API request, and we can do it all within a single file.

Set Up

We are going to reuse our initGlobalTracer() and constructor from earlier, but let’s make a new class called TracerInterceptor that implements HttpInterceptor. You can put this anywhere, I put mine inside of an interceptors/ folder. Your class will have errors as you haven’t implemented the interface yet, that’s okay.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Injectable } from '@angular/core';
import {
  HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse, HttpErrorResponse
} from '@angular/common/http';

import { Observable } from 'rxjs';
import { finalize, tap } from 'rxjs/operators';
import * as opentracing from 'opentracing';
import * as lightstepTracer from 'lightstep-tracer';

@Injectable()
export class TracerInterceptor implements HttpInterceptor {

  constructor() {
    this.initGlobalTracer('YOUR_ACCESS_TOKEN', 'TraceInterceptor');
  }

  initGlobalTracer(accessToken: string, componentName: string) {
    const options: lightstepTracer.TracerOptions = {
      access_token: accessToken,
      component_name: componentName
    };
    opentracing.initGlobalTracer( new lightstepTracer.Tracer(options));
  }
}

Because this is an Interceptor, we will need to provide it to our application. This is a very standardly configured example app so we will provide our interceptor in our AppModule. Feel free to put yours wherever, but don’t get stuck at this part, we’re just setting up.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http';
import { TracerInterceptor } from './interceptor/tracer.interceptor';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [{ provide: HTTP_INTERCEPTORS, useClass: TracerInterceptor, multi: true }, ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Cool, so now we need to implementintercept() and it’s helpers. Feel free to copy and paste the following three functions and then I’ll talk about what I’m doing here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {
    const span = opentracing.globalTracer().startSpan(this.getName(req));
    const tracedReq = this.injectContext(span, req);
    return next.handle(tracedReq)
    .pipe(
        tap(
            (event: HttpEvent<any>) => {
                if (event instanceof HttpResponse) {
                    span.log(event.body);
                }
            },
            (event: HttpErrorResponse) => {
                if (event instanceof HttpErrorResponse) {
                    span.setTag('error', true);
                    span.log(event);
                }
            }
          ),
        finalize(() => {
            span.finish();
        })
    );
  }

  injectContext(span: opentracing.Span, req: HttpRequest<any> ): HttpRequest<any> {
    const carrier = {};
    opentracing.globalTracer().inject(span.context(), opentracing.FORMAT_TEXT_MAP, carrier);
    const clone = req.clone({
      headers: req.headers
      .set('ot-tracer-sampled', carrier['ot-tracer-sampled'])
      .set('ot-tracer-spanid', carrier['ot-tracer-spanid'])
      .set('ot-tracer-traceid', carrier['ot-tracer-traceid'])
    });
    return clone;
  }

  getName(req: HttpRequest<any>): string {
    if (req.headers.has('traceOperationName')) {
        return req.headers.get('traceOperationName');
    } else {
        return req.url;
    }
  }

First, design decisions. My goal here was to make sure that each API/Request shows up as it’s own operation within LightStep, but is also configurable to a degree. That’s where getName() plays it’s part.

injectContext() is needed because we want our traces in our UI to be properly correlated with the traces in the services downstream.

The intercept() method itself is very simple, we check if we got a response and log that on our span, or we check if we received an error and log that on our span. The difference in our error case is that we set a Tag of error to true.

The full class for clarity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { Injectable } from '@angular/core';
import {
  HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse, HttpErrorResponse
} from '@angular/common/http';

import { Observable } from 'rxjs';
import { finalize, tap } from 'rxjs/operators';
import * as opentracing from 'opentracing';
import * as lightstepTracer from 'lightstep-tracer';

@Injectable()
export class TracerInterceptor implements HttpInterceptor {

  constructor() {
    this.initGlobalTracer('YOUR_ACCESS_TOKEN', 'TraceInterceptor');
  }

  initGlobalTracer(accessToken: string, componentName: string) {
    const options: lightstepTracer.TracerOptions = {
      access_token: accessToken,
      component_name: componentName
    };
    opentracing.initGlobalTracer( new lightstepTracer.Tracer(options));
  }

  intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {
    const span = opentracing.globalTracer().startSpan(this.getName(req));
    const tracedReq = this.injectContext(span, req);
    return next.handle(tracedReq)
    .pipe(
        tap(
            (event: HttpEvent<any>) => {
                if (event instanceof HttpResponse) {
                    span.log(event.body);
                }
            },
            (event: HttpErrorResponse) => {
                if (event instanceof HttpErrorResponse) {
                    span.setTag('error', true);
                    span.log(event);
                }
            }
          ),
        finalize(() => {
            span.finish();
        })
    );
  }

  injectContext(span: opentracing.Span, req: HttpRequest<any> ): HttpRequest<any> {
    const carrier = {};
    opentracing.globalTracer().inject(span.context(), opentracing.FORMAT_TEXT_MAP, carrier);
    const clone = req.clone({
      headers: req.headers
      .set('ot-tracer-sampled', carrier['ot-tracer-sampled'])
      .set('ot-tracer-spanid', carrier['ot-tracer-spanid'])
      .set('ot-tracer-traceid', carrier['ot-tracer-traceid'])
    });
    return clone;
  }

  getName(req: HttpRequest<any>): string {
    if (req.headers.has('traceOperationName')) {
        return req.headers.get('traceOperationName');
    } else {
        return req.url;
    }
  }
}

That’s it! 🙂

Extending Our Interceptor

Obviously, this is just a code snippet and not a plugin, so you can modify this in any way that makes sense for you. With tracing, how you identify operations and tagging schemes is ultimately up to you. In my next post, I’ll share how to add configuration that allows for sets of tags for success and error states.