Observability and Working with Micro Frontends

Micro frontend applications present a challenge for frontend observability libraries. No single solution exists to make OpenTelemetry or the Honeycomb Web SDK work easily with micro frontend. You will need to consider tradeoffs and limitations, so you’ll need a clear understanding of your micro frontend architecture and your observability needs. To start, ask yourself:

  • What does my micro frontend architecture look like?
    • How is it unique?
    • How is it similar to other micro frontend systems? (For example, it uses module federation.)
  • What sort of information am I observing?
  • At what granularity am I observing this information?

Read on for some ideas about how to manage the challenges of adding observability to your micro frontend.

Initializing Libraries 

To initialize libraries, micro frontend systems usually use either module federation or a central bootstrap. Some systems even use both approaches. Systems using module federation are much more likely to encounter issues while initializing observability libraries, whereas issues are less of a problem with micro frontends that use a central bootstrap approach.

Most micro frontends will encounter problems related to the observability SDK being a singleton. OpenTelemetry libraries and the Honeycomb Web SDK must be initialized only once per page load, which can be a problem in micro frontend architectures where every module is attempting to initialize its own OpenTelemetry library or Honeycomb Web SDK. If your system allows, initialize the library in a central bootstrap or shared module, and expose it to the other modules.

Some module federation systems allow you to specify “singleton dependencies” for libraries like OpenTelemetry or React.js, which require that they be singletons on the page. This example shows webpack with the ModuleFederation plugin:

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      shared: {
        // Adds HoneycombWebSDK and OpenTelemetry as shared modules
        "@honeycombio/opentelemetry-web": {
          singleton: true,
        },
        "@opentelemetry/api": {
          singleton: true,
        }
      },
    }),
  ],
};

// Initializes HoneycombWebSDK (only do this in one module)
const sdk = new HoneycombWebSDK({
  apiKey: "your-honeycomb-api-key",
  serviceName: "hfo-microfrontends",
  instrumentations: [getWebAutoInstrumentations()],
  localVisualizations: true,
});

The Honeycomb Web SDK will be initialized in the main module, and you can use the OpenTelemetry API for custom instrumentation. In other modules, you can create traces and spans as you normally would.

Understanding Your Observability Needs 

The sort of challenges you face will depend on what information you need visibility into and how granular you want to be with attributing things to modules:

  • What metrics are done on a per-page basis?
  • Which metrics should be granular enough to go down to the module, or even the component, level?

Some metrics might be fine at a per-page level. But for many metrics, such as error reporting, you’ll likely want to identify the modules from which the errors came.

Custom Instrumentation 

One way to avoid issues is to write your own code to handle instrumentation instead of using automatic instrumentation. If most of your telemetry uses custom instrumentation, you may be able to more easily add attributes for identifying specific modules.

Auto-instrumentation 

When it comes to automatic instrumentation, most observability libraries will treat the entire application as a monolith. If you are only concerned with page-level granularity–for example, when tracking Web Core Vitals per page–this might be fine.

Getting more granular data, especially at the module level, can be more challenging. You could use events like user clicks to identify the module the event came from. Check out the section on custom span processing with the Honeycomb Web SDK for more custom span processor examples.

class MFOSpanProcessor implements SpanProcessor {

  constructor(){
     super();
  }
    
  onStart(span: Span) {
    let moduleName = 'unknown-module';

    if(isModuleA()) {
        moduleName = 'module-a';
    }
    else if(isModuleB()) {
        moduleName = 'module-b';
    }
    else if(isMainModule()) {
        moduleName = 'module-main';
    }
    span.setAttributes({'module': moduleName});
  }
    
}

//...
const sdk = new HoneycombWebSDK({
  apiKey: "your-honeycomb-api-key",
  serviceName: "hfo-micorfrontends",
  instrumentations: [getWebAutoInstrumentations()],
  spanProcessors: [new MFOSpanProcessor()]
  localVisualizations: true,
});

The method by which you figure out which module the span comes from will depend on your implementation. Sometimes you can use pre-existing attributes, such as the target element to determine origin.