11package com.dprint.formatter
22
33import com.dprint.i18n.DprintBundle
4+ import com.dprint.otel.AttributeKeys
5+ import com.dprint.otel.DprintScope
46import com.dprint.services.editorservice.EditorServiceManager
57import com.dprint.services.editorservice.FormatResult
68import com.dprint.services.editorservice.exceptions.ProcessUnavailableException
@@ -11,6 +13,14 @@ import com.intellij.formatting.service.AsyncFormattingRequest
1113import com.intellij.openapi.diagnostic.logger
1214import com.intellij.openapi.project.Project
1315import com.intellij.openapi.util.TextRange
16+ import com.intellij.platform.diagnostic.telemetry.TelemetryManager
17+ import com.intellij.platform.diagnostic.telemetry.helpers.use
18+ import io.opentelemetry.api.common.AttributeKey
19+ import io.opentelemetry.api.common.Attributes
20+ import io.opentelemetry.api.trace.Span
21+ import io.opentelemetry.api.trace.StatusCode
22+ import io.opentelemetry.api.trace.Tracer
23+ import io.opentelemetry.context.Context
1424import java.util.concurrent.CancellationException
1525import java.util.concurrent.CompletableFuture
1626import java.util.concurrent.ExecutionException
@@ -28,72 +38,143 @@ class DprintFormattingTask(
2838) {
2939 private var formattingIds = mutableListOf<Int >()
3040 private var isCancelled = false
41+ private val tracer: Tracer = TelemetryManager .getInstance().getTracer(DprintScope .FormatterScope )
3142
3243 /* *
3344 * Used when we want to cancel a format, so that we can cancel every future in the chain.
3445 */
3546 private val allFormatFutures = mutableListOf<CompletableFuture <FormatResult >>()
3647
3748 fun run () {
38- val content = formattingRequest.documentText
39- val ranges =
40- if (editorServiceManager.canRangeFormat()) {
41- formattingRequest.formattingRanges
42- } else {
43- mutableListOf (
44- TextRange (0 , content.length),
49+ val rootSpan =
50+ tracer.spanBuilder(" dprint.format" )
51+ .setAttribute(AttributeKeys .FILE_PATH , path)
52+ .startSpan()
53+
54+ try {
55+ rootSpan.makeCurrent().use { scope ->
56+ val content = formattingRequest.documentText
57+
58+ rootSpan.setAttribute(AttributeKeys .CONTENT_LENGTH , content.length.toLong())
59+
60+ val ranges =
61+ tracer.spanBuilder(" dprint.determine_ranges" )
62+ .startSpan().use { rangesSpan ->
63+ if (editorServiceManager.canRangeFormat()) {
64+ rangesSpan.setAttribute(" range_format_supported" , true )
65+ rangesSpan.setAttribute(
66+ " ranges_count" ,
67+ formattingRequest.formattingRanges.size.toLong(),
68+ )
69+ formattingRequest.formattingRanges
70+ } else {
71+ rangesSpan.setAttribute(" range_format_supported" , false )
72+ rangesSpan.setAttribute(" ranges_count" , 1L )
73+ mutableListOf (
74+ TextRange (0 , content.length),
75+ )
76+ }
77+ }
78+
79+ infoLogWithConsole(
80+ DprintBundle .message(" external.formatter.running.task" , path),
81+ project,
82+ LOGGER ,
4583 )
46- }
4784
48- infoLogWithConsole(
49- DprintBundle .message(" external.formatter.running.task" , path),
50- project,
51- LOGGER ,
52- )
85+ val initialResult = FormatResult (formattedContent = content)
86+ val baseFormatFuture = CompletableFuture .completedFuture(initialResult)
87+ allFormatFutures.add(baseFormatFuture)
5388
54- val initialResult = FormatResult (formattedContent = content)
55- val baseFormatFuture = CompletableFuture .completedFuture(initialResult)
56- allFormatFutures.add(baseFormatFuture)
57-
58- var nextFuture = baseFormatFuture
59- for (range in ranges.subList(0 , ranges.size)) {
60- nextFuture.thenCompose { formatResult ->
61- nextFuture =
62- if (isCancelled) {
63- // Revert to the initial contents
64- CompletableFuture .completedFuture(initialResult)
65- } else {
66- applyNextRangeFormat(
67- path,
68- formatResult,
69- getStartOfRange(formatResult.formattedContent, content, range),
70- getEndOfRange(formatResult.formattedContent, content, range),
71- )
89+ val formatRangesSpan =
90+ tracer.spanBuilder(" dprint.format_ranges" )
91+ .setAttribute(" ranges_count" , ranges.size.toLong())
92+ .startSpan()
93+
94+ var nextFuture = baseFormatFuture
95+ for (range in ranges.subList(0 , ranges.size)) {
96+ formatRangesSpan.addEvent(
97+ " processing_range" ,
98+ Attributes .of(
99+ AttributeKeys .RANGE_START ,
100+ range.startOffset.toLong(),
101+ AttributeKeys .RANGE_END ,
102+ range.endOffset.toLong(),
103+ ),
104+ )
105+
106+ nextFuture.thenCompose { formatResult ->
107+ nextFuture =
108+ if (isCancelled) {
109+ formatRangesSpan.addEvent(
110+ " format_cancelled" ,
111+ Attributes .of(
112+ AttributeKeys .RANGE_START ,
113+ range.startOffset.toLong(),
114+ AttributeKeys .RANGE_END ,
115+ range.endOffset.toLong(),
116+ ),
117+ )
118+ // Revert to the initial contents
119+ CompletableFuture .completedFuture(initialResult)
120+ } else {
121+ applyNextRangeFormat(
122+ path,
123+ formatResult,
124+ getStartOfRange(formatResult.formattedContent, content, range),
125+ getEndOfRange(formatResult.formattedContent, content, range),
126+ formatRangesSpan,
127+ )
128+ }
129+ nextFuture
72130 }
73- nextFuture
74- }
75- }
131+ }
76132
77- // Timeouts are handled at the EditorServiceManager level and an empty result will be
78- // returned if something goes wrong
79- val result = getFuture(nextFuture)
133+ // Timeouts are handled at the EditorServiceManager level and an empty result will be
134+ // returned if something goes wrong
135+ val result = getFuture(nextFuture)
136+ formatRangesSpan.end()
80137
81- // If cancelled there is no need to utilise the formattingRequest finalising methods
82- if (isCancelled) return
138+ // If cancelled there is no need to utilise the formattingRequest finalising methods
139+ if (isCancelled) {
140+ rootSpan.addEvent(" formatting_cancelled" )
141+ return
142+ }
83143
84- // If the result is null we don't want to change the document text, so we just set it to be the original.
85- // This should only happen if getting the future throws.
86- if (result == null ) {
87- formattingRequest.onTextReady(content)
88- return
89- }
144+ val resultSpan =
145+ tracer.spanBuilder(" dprint.process_result" )
146+ .startSpan()
147+
148+ resultSpan.use {
149+ // If the result is null we don't want to change the document text, so we just set it to be the original.
150+ // This should only happen if getting the future throws.
151+ if (result == null ) {
152+ resultSpan.setAttribute(" result" , " null" )
153+ formattingRequest.onTextReady(content)
154+ return
155+ }
156+
157+ val error = result.error
158+ if (error != null ) {
159+ resultSpan.setStatus(StatusCode .ERROR , error)
160+ formattingRequest.onError(DprintBundle .message(" formatting.error" ), error)
161+ } else {
162+ // Record if there was any content change
163+ resultSpan.setAttribute(" content_changed" , (result.formattedContent != content))
90164
91- val error = result.error
92- if (error != null ) {
93- formattingRequest.onError(DprintBundle .message(" formatting.error" ), error)
94- } else {
95- // If the result is a no op it will be null, in which case we pass the original content back in
96- formattingRequest.onTextReady(result.formattedContent ? : content)
165+ // If the result is a no op it will be null, in which case we pass the original content back in
166+ val finalContent = result.formattedContent ? : content
167+ resultSpan.setAttribute(" final_content_length" , finalContent.length.toLong())
168+ formattingRequest.onTextReady(finalContent)
169+ }
170+ }
171+ }
172+ } catch (e: Exception ) {
173+ rootSpan.recordException(e)
174+ rootSpan.setStatus(StatusCode .ERROR )
175+ throw e
176+ } finally {
177+ rootSpan.end()
97178 }
98179 }
99180
@@ -134,6 +215,7 @@ class DprintFormattingTask(
134215 previousFormatResult : FormatResult ,
135216 startIndex : Int? ,
136217 endIndex : Int? ,
218+ parentSpan : Span ,
137219 ): CompletableFuture <FormatResult >? {
138220 val contentToFormat = previousFormatResult.formattedContent
139221 if (contentToFormat == null || startIndex == null || endIndex == null ) {
@@ -150,15 +232,37 @@ class DprintFormattingTask(
150232 return null
151233 }
152234
235+ val span =
236+ tracer.spanBuilder(" dprint.formatter.apply_range_format" )
237+ .setParent(Context .current().with (parentSpan))
238+ .setAttribute(AttributeKeys .FILE_PATH , path)
239+ .setAttribute(AttributeKeys .RANGE_START , startIndex.toLong())
240+ .setAttribute(AttributeKeys .RANGE_END , endIndex.toLong())
241+ .setAttribute(AttributeKeys .CONTENT_LENGTH , contentToFormat.length.toLong())
242+ .startSpan()
243+
153244 // Need to update the formatting id so the correct job would be cancelled
154245 val formattingId = editorServiceManager.maybeGetFormatId()
155246 formattingId?.let {
156247 formattingIds.add(it)
248+ span.setAttribute(AttributeKeys .FORMATTING_ID , it.toLong())
157249 }
158250
159251 val nextFuture = CompletableFuture <FormatResult >()
160252 allFormatFutures.add(nextFuture)
161253 val nextHandler: (FormatResult ) -> Unit = { nextResult ->
254+ // Add result information to the span
255+ if (nextResult.error != null ) {
256+ span.setStatus(StatusCode .ERROR , nextResult.error)
257+ } else {
258+ span.setStatus(StatusCode .OK )
259+ if (nextResult.formattedContent != null ) {
260+ val contentLengthDiff = nextResult.formattedContent.length - contentToFormat.length
261+ span.setAttribute(" content_length_diff" , contentLengthDiff.toLong())
262+ }
263+ }
264+ span.end()
265+
162266 nextFuture.complete(nextResult)
163267 }
164268 editorServiceManager.format(
@@ -174,21 +278,40 @@ class DprintFormattingTask(
174278 }
175279
176280 fun cancel (): Boolean {
177- if (! editorServiceManager.canCancelFormat()) return false
281+ val span =
282+ tracer.spanBuilder(" dprint.formatter.cancel_format" )
283+ .setAttribute(AttributeKeys .FILE_PATH , path)
284+ .startSpan()
178285
179- isCancelled = true
180- for (id in formattingIds) {
181- infoLogWithConsole(
182- DprintBundle .message(" external.formatter.cancelling.task" , id),
183- project,
184- LOGGER ,
286+ span.use { scope ->
287+ if (! editorServiceManager.canCancelFormat()) {
288+ span.setAttribute(" can_cancel" , false )
289+ span.addEvent(" cancel_not_supported" )
290+ return false
291+ }
292+
293+ span.setAttribute(" can_cancel" , true )
294+ span.setAttribute(" formatting_ids_count" , formattingIds.size.toLong())
295+
296+ isCancelled = true
297+ for (id in formattingIds) {
298+ span.addEvent(" cancelling_task" , Attributes .of(AttributeKeys .FORMATTING_ID , id.toLong()))
299+ infoLogWithConsole(
300+ DprintBundle .message(" external.formatter.cancelling.task" , id),
301+ project,
302+ LOGGER ,
303+ )
304+ editorServiceManager.cancelFormat(id)
305+ }
306+
307+ // Clean up state so process can complete
308+ span.addEvent(
309+ " cancelling_futures" ,
310+ Attributes .of(AttributeKey .longKey(" futures_count" ), allFormatFutures.size.toLong()),
185311 )
186- editorServiceManager.cancelFormat(id)
312+ allFormatFutures.stream().forEach { f -> f.cancel(true ) }
313+ return true
187314 }
188-
189- // Clean up state so process can complete
190- allFormatFutures.stream().forEach { f -> f.cancel(true ) }
191- return true
192315 }
193316
194317 fun isRunUnderProgress (): Boolean {
0 commit comments