diff --git a/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry/ZbTelemetryExtensions.cs b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry/ZbTelemetryExtensions.cs
new file mode 100644
index 0000000..54442c3
--- /dev/null
+++ b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry/ZbTelemetryExtensions.cs
@@ -0,0 +1,132 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+namespace ZB.MOM.WW.Telemetry;
+
+///
+/// Extension point for configuring the OpenTelemetry metrics + traces bootstrap on an
+/// (or directly on an ).
+/// Wires the shared Resource, standard instrumentation, the app's own Meters and
+/// ActivitySources, and the selected exporter. Does NOT configure Serilog.
+///
+public static class ZbTelemetryExtensions
+{
+ ///
+ /// Configures the OpenTelemetry MeterProvider and TracerProvider with the shared Resource,
+ /// standard instrumentation (ASP.NET Core, HttpClient, gRPC client, runtime, process), the
+ /// app's own Meters and ActivitySources, and the selected exporter.
+ ///
+ /// The host application builder.
+ /// Populates the .
+ public static IHostApplicationBuilder AddZbTelemetry(
+ this IHostApplicationBuilder builder,
+ Action configure)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ builder.Services.AddZbTelemetry(BuildOptions(configure));
+ return builder;
+ }
+
+ ///
+ /// overload for contexts where
+ /// is not available. Requires the caller to supply a
+ /// pre-built .
+ ///
+ /// The service collection.
+ /// The fully-populated telemetry options.
+ public static IServiceCollection AddZbTelemetry(
+ this IServiceCollection services,
+ ZbTelemetryOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(options);
+
+ services.AddOpenTelemetry()
+ .ConfigureResource(rb => ZbResource.Configure(rb, options))
+ .WithMetrics(metrics =>
+ {
+ foreach (var meter in options.Meters)
+ {
+ metrics.AddMeter(meter);
+ }
+
+ metrics
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation()
+ .AddProcessInstrumentation();
+
+ ApplyMetricsExporter(metrics, options);
+ })
+ .WithTracing(tracing =>
+ {
+ foreach (var source in options.ActivitySources)
+ {
+ tracing.AddSource(source);
+ }
+
+ tracing
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddGrpcClientInstrumentation();
+
+ ApplyTracingExporter(tracing, options);
+ });
+
+ return services;
+ }
+
+ ///
+ /// IServiceCollection overload that accepts a configure delegate (convenience for callers
+ /// that only have an but prefer the lambda form).
+ ///
+ /// The service collection.
+ /// Populates the .
+ public static IServiceCollection AddZbTelemetry(
+ this IServiceCollection services,
+ Action configure) =>
+ services.AddZbTelemetry(BuildOptions(configure));
+
+ private static ZbTelemetryOptions BuildOptions(Action configure)
+ {
+ ArgumentNullException.ThrowIfNull(configure);
+ var options = new ZbTelemetryOptions();
+ configure(options);
+ return options;
+ }
+
+ private static void ApplyMetricsExporter(MeterProviderBuilder metrics, ZbTelemetryOptions options)
+ {
+ switch (options.Exporter)
+ {
+ case ZbExporter.Otlp:
+ metrics.AddOtlpExporter(o => ConfigureOtlp(o, options));
+ break;
+ case ZbExporter.Prometheus:
+ default:
+ metrics.AddPrometheusExporter();
+ break;
+ }
+ }
+
+ private static void ApplyTracingExporter(TracerProviderBuilder tracing, ZbTelemetryOptions options)
+ {
+ // Prometheus is metrics-only; traces have no Prometheus path. Only OTLP exports traces.
+ if (options.Exporter == ZbExporter.Otlp)
+ {
+ tracing.AddOtlpExporter(o => ConfigureOtlp(o, options));
+ }
+ }
+
+ private static void ConfigureOtlp(
+ OpenTelemetry.Exporter.OtlpExporterOptions otlp,
+ ZbTelemetryOptions options)
+ {
+ if (!string.IsNullOrEmpty(options.OtlpEndpoint))
+ {
+ otlp.Endpoint = new Uri(options.OtlpEndpoint);
+ }
+ }
+}
diff --git a/ZB.MOM.WW.Telemetry/tests/ZB.MOM.WW.Telemetry.Tests/AddZbTelemetryTests.cs b/ZB.MOM.WW.Telemetry/tests/ZB.MOM.WW.Telemetry.Tests/AddZbTelemetryTests.cs
new file mode 100644
index 0000000..7aee2ce
--- /dev/null
+++ b/ZB.MOM.WW.Telemetry/tests/ZB.MOM.WW.Telemetry.Tests/AddZbTelemetryTests.cs
@@ -0,0 +1,82 @@
+using System.Diagnostics.Metrics;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+using ZB.MOM.WW.Telemetry;
+
+namespace ZB.MOM.WW.Telemetry.Tests;
+
+public sealed class AddZbTelemetryTests
+{
+ [Fact]
+ public void AddZbTelemetry_ExportsAppMeter_WithSharedResource()
+ {
+ // 1.15.x note: AddInMemoryExporter moved out of the core OpenTelemetry assembly into a
+ // separate OpenTelemetry.Exporter.InMemory package (not referenced here). We attach a
+ // BaseExporter directly instead — it both collects metric names and exposes the
+ // MeterProvider Resource via ParentProvider.GetResource().
+ var capture = new CapturingMetricExporter();
+
+ var builder = WebApplication.CreateBuilder();
+ builder.AddZbTelemetry(o =>
+ {
+ o.ServiceName = "t";
+ o.SiteId = "site-test";
+ o.NodeRole = "central";
+ o.Meters = ["Test.Meter"];
+ });
+
+ // Compose a capturing reader onto the pipeline AddZbTelemetry already registered.
+ builder.Services.ConfigureOpenTelemetryMeterProvider(b =>
+ b.AddReader(new PeriodicExportingMetricReader(capture)
+ {
+ TemporalityPreference = MetricReaderTemporalityPreference.Cumulative,
+ }));
+
+ // Create the meter + instrument BEFORE the provider is built so the MeterProvider's
+ // listener subscribes to it during construction.
+ using var meter = new Meter("Test.Meter");
+ var counter = meter.CreateCounter("test.events.count");
+
+ using var app = builder.Build();
+
+ var meterProvider = app.Services.GetRequiredService();
+ counter.Add(1);
+ meterProvider.ForceFlush();
+
+ // The app's meter was registered and its instrument was collected through the pipeline.
+ Assert.Contains("test.events.count", capture.MetricNames);
+
+ // The exported metric carries the shared Resource (identical to ZbResource.Build).
+ Assert.NotNull(capture.CapturedResource);
+ var attrs = capture.CapturedResource!.Attributes.ToDictionary(a => a.Key, a => a.Value);
+ Assert.Equal("t", attrs["service.name"]);
+ Assert.Equal("ZB.MOM.WW", attrs["service.namespace"]);
+ Assert.Equal("site-test", attrs["site.id"]);
+ Assert.Equal("central", attrs["node.role"]);
+ Assert.Equal(Environment.MachineName, attrs["host.name"]);
+ }
+
+ ///
+ /// Collects exported metric names and captures the MeterProvider Resource on first export so
+ /// the test can assert the pipeline wired both the app meter and the shared Resource.
+ ///
+ private sealed class CapturingMetricExporter : BaseExporter
+ {
+ public List MetricNames { get; } = [];
+ public Resource? CapturedResource { get; private set; }
+
+ public override ExportResult Export(in Batch batch)
+ {
+ CapturedResource ??= ParentProvider?.GetResource();
+ foreach (var metric in batch)
+ {
+ MetricNames.Add(metric.Name);
+ }
+
+ return ExportResult.Success;
+ }
+ }
+}