Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1f43834
added quartz scheduler listener instrumentation for scheduler errors
angad-2 Jul 2, 2026
cb5f693
added quartz.scheduler.error.message, populated from request.getMessa…
angad-2 Jul 2, 2026
9928f89
added scheduler error in the other test methods and the success and f…
angad-2 Jul 2, 2026
5fdd6a8
swicthed to emitting an event via logs api
angad-2 Jul 6, 2026
17e6fa1
applied formatting to test
angad-2 Jul 6, 2026
239d543
swicthed to setEventName
angad-2 Jul 8, 2026
715ef69
moved error message into the log body
angad-2 Jul 9, 2026
b1c00ca
added quartz scheduler name to job execution spans, added setExceptio…
angad-2 Jul 9, 2026
87c54c5
fixed empty catch block
angad-2 Jul 9, 2026
afca3f9
suppress scheduler error only during job execution via context marker
angad-2 Jul 11, 2026
26ca9f4
extracted listener checks, used exact attribute assertions
angad-2 Jul 13, 2026
c39a338
renamed experimental config to emit-experimental-telemetry, deprecate…
angad-2 Jul 13, 2026
6f229b0
Address review comments from copilot-pull-request-reviewer: test sche…
trask Jul 19, 2026
ed4606a
Address review comment from copilot-pull-request-reviewer: document d…
trask Jul 19, 2026
9cfeaed
Address review comments from copilot-pull-request-reviewer: document …
trask Jul 19, 2026
375b45d
Merge branch 'main' into main-angad
trask Jul 19, 2026
1c051f6
Address review comment from copilot-pull-request-reviewer: document Q…
trask Jul 19, 2026
c03dec1
Address review comment from copilot-pull-request-reviewer: preserve d…
trask Jul 19, 2026
daf9675
Address review comment from copilot-pull-request-reviewer: gate sched…
trask Jul 20, 2026
a6401da
address review comment from copilot-pull-request-reviewer: suppress o…
angad-2 Jul 21, 2026
3e795a2
Fix Quartz scheduler error regression test
trask Jul 22, 2026
b67cd69
Merge remote-tracking branch 'upstream/main' into main-angad
trask Jul 22, 2026
9758f71
fix: apply spotless formatting and align README table
angad-2 Jul 29, 2026
ab9f715
Merge branch 'main' into main-angad
angad-2 Aug 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@
package io.opentelemetry.instrumentation.quartz.v2_0;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import org.quartz.JobKey;
import org.quartz.JobListener;
import org.quartz.Matcher;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerListener;
import org.quartz.impl.matchers.EverythingMatcher;

/** Entrypoint for telemetry instrumentation of Quartz jobs. */
public final class QuartzTelemetry {
private final JobListener jobListener;
private final Instrumenter<SchedulerError, Void> schedulerErrorInstrumenter;

/** Returns a new {@link QuartzTelemetry} configured with the given {@link OpenTelemetry}. */
public static QuartzTelemetry create(OpenTelemetry openTelemetry) {
Expand All @@ -29,8 +32,10 @@ public static QuartzTelemetryBuilder builder(OpenTelemetry openTelemetry) {
return new QuartzTelemetryBuilder(openTelemetry);
}

QuartzTelemetry(JobListener jobListener) {
QuartzTelemetry(
JobListener jobListener, Instrumenter<SchedulerError, Void> schedulerErrorInstrumenter) {
this.jobListener = jobListener;
this.schedulerErrorInstrumenter = schedulerErrorInstrumenter;
}

/**
Expand All @@ -42,6 +47,11 @@ public static QuartzTelemetryBuilder builder(OpenTelemetry openTelemetry) {
* not throw exceptions.
*/
public void configure(Scheduler scheduler) {
addJobListener(scheduler);
addSchedulerListener(scheduler);
}

private void addJobListener(Scheduler scheduler) {
try {
for (JobListener listener : scheduler.getListenerManager().getJobListeners()) {
if (listener instanceof TracingJobListener) {
Expand All @@ -61,4 +71,27 @@ public void configure(Scheduler scheduler) {
throw new IllegalStateException("Could not add JobListener to Scheduler", e);
}
}

private void addSchedulerListener(Scheduler scheduler) {
try {
for (SchedulerListener listener : scheduler.getListenerManager().getSchedulerListeners()) {
if (listener instanceof TracingSchedulerListener) {
return;
}
}
} catch (SchedulerException ignored) {
// Ignore
}
Comment thread
laurit marked this conversation as resolved.
Outdated
Comment thread
trask marked this conversation as resolved.
Outdated
try {
// The scheduler name is only available here (at configuration time), so we capture it now
// and hand it to the listener to use as a span attribute.
String schedulerName = scheduler.getSchedulerName();
scheduler
.getListenerManager()
.addSchedulerListener(
new TracingSchedulerListener(schedulerErrorInstrumenter, schedulerName));
} catch (SchedulerException e) {
throw new IllegalStateException("Could not add SchedulerListener to Scheduler", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ public QuartzTelemetry build() {
CodeAttributesExtractor.create(new QuartzCodeAttributesGetter()));
instrumenter.addAttributesExtractors(additionalExtractors);

return new QuartzTelemetry(new TracingJobListener(instrumenter.buildInstrumenter()));
// Separate instrumenter for scheduler-level events: the request type differs from job
// execution, and these are point-in-time events rather than start/end-bracketed work.
Instrumenter<SchedulerError, Void> schedulerErrorInstrumenter =
Instrumenter.<SchedulerError, Void>builder(
openTelemetry, INSTRUMENTATION_NAME, request -> "Quartz scheduler error")
.setErrorCauseExtractor(new QuartzErrorCauseExtractor())
.addAttributesExtractor(new SchedulerErrorAttributesExtractor())
.buildInstrumenter();

return new QuartzTelemetry(
new TracingJobListener(instrumenter.buildInstrumenter()), schedulerErrorInstrumenter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.quartz.v2_0;

import javax.annotation.Nullable;

/**
* A scheduler-level error reported by Quartz through {@code SchedulerListener.schedulerError}. This
* is the "request" object that drives the scheduler-error {@link
* io.opentelemetry.instrumentation.api.instrumenter.Instrumenter}.
*/
final class SchedulerError {

private final String schedulerName;
@Nullable private final String message;

SchedulerError(String schedulerName, @Nullable String message) {
this.schedulerName = schedulerName;
this.message = message;
}

String getSchedulerName() {
return schedulerName;
}

@Nullable
String getMessage() {
return message;
}
Comment thread
angad-2 marked this conversation as resolved.
Outdated
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.quartz.v2_0;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import javax.annotation.Nullable;

final class SchedulerErrorAttributesExtractor implements AttributesExtractor<SchedulerError, Void> {

// Experimental attribute: name/shape may change until scheduler instrumentation stabilizes.
private static final AttributeKey<String> SCHEDULER_NAME =
AttributeKey.stringKey("quartz.scheduler.name");

@Override
public void onStart(AttributesBuilder attributes, Context parentContext, SchedulerError request) {
attributes.put(SCHEDULER_NAME, request.getSchedulerName());
}

@Override
public void onEnd(
AttributesBuilder attributes,
Context context,
SchedulerError request,
@Nullable Void response,
@Nullable Throwable error) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.quartz.v2_0;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import org.quartz.SchedulerException;
import org.quartz.listeners.SchedulerListenerSupport;

/**
* Instruments scheduler-level Quartz events. Extends {@link SchedulerListenerSupport} so we only
* override the hooks we care about; every other {@code SchedulerListener} method defaults to a
* no-op and is available as an extension point for future events.
*/
final class TracingSchedulerListener extends SchedulerListenerSupport {

private final Instrumenter<SchedulerError, Void> instrumenter;
private final String schedulerName;

TracingSchedulerListener(Instrumenter<SchedulerError, Void> instrumenter, String schedulerName) {
this.instrumenter = instrumenter;
this.schedulerName = schedulerName;
}

@Override
public void schedulerError(String msg, SchedulerException cause) {
Context parentContext = Context.current();

// Quartz also reports job execution failures through schedulerError, on the same thread while
// the job execution span is still current. That failure is already recorded on the job span, so
// emitting another span here would just duplicate it. Only instrument genuine scheduler-level
// errors, i.e. when no span is currently active.
if (Span.fromContext(parentContext).getSpanContext().isValid()) {
return;
Comment thread
trask marked this conversation as resolved.
Outdated
}

SchedulerError request = new SchedulerError(schedulerName, msg);
if (!instrumenter.shouldStart(parentContext, request)) {
return;
}

// schedulerError is a point-in-time event with no start/end bracket around work, so we start
Comment thread
trask marked this conversation as resolved.
Outdated
// and immediately end a span that records the failure. The span has no children but surfaces
// the scheduler error (and the SchedulerException) in traces.
Context context = instrumenter.start(parentContext, request);
instrumenter.end(context, request, null, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.quartz.JobExecutionContext;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerListener;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;

Expand Down Expand Up @@ -141,4 +142,28 @@ public void execute(JobExecutionContext context) {
throw new IllegalStateException("Bad job");
}
}

@Test
Comment thread
angad-2 marked this conversation as resolved.
Outdated
void schedulerError() throws SchedulerException {
SchedulerException cause = new SchedulerException("boom");

// A real scheduler error is hard to provoke deterministically in a unit test, so we invoke the
// registered SchedulerListener directly to verify the span it produces.
for (SchedulerListener listener : scheduler.getListenerManager().getSchedulerListeners()) {
listener.schedulerError("Something went wrong", cause);
}

getTesting()
.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
span ->
span.hasName("Quartz scheduler error")
.hasKind(SpanKind.INTERNAL)
.hasNoParent()
.hasStatus(StatusData.error())
.hasException(cause)
.hasAttributesSatisfyingExactly(
equalTo(stringKey("quartz.scheduler.name"), "default"))));
}
}
Loading