Repository navigation
Replies: 1 comment
|
The complete TAL rule files of this proposal, one per listener or processor they port. Together they are the native
|
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
This SWIP moves SkyWalking's native trace analysis, the Java listeners in
agent-analyzer, to TAL rules. It is aport: no output change and no logic change, with one documented exception. This post is the general design; the first
comment holds the complete TAL rule files of the port. The full text, about twice this post, lands as
docs/en/swip/SWIP-17/SWIP.mdwith the rule files once the design is agreed.Motivation
SkyWalking analyzes traces with two engines. Spans that arrive over OTLP are analyzed by
TAL rules, which an operator can read and edit. Segments that SkyWalking's own
agents send are analyzed by Java listeners in the
agent-analyzermodule. Those listeners turn each span into OALsources (
Service,ServiceInstance,Endpoint, the relations,DatabaseAccessand others) forcore.oaland theother
.oalfiles, write the trace itself, and hand GenAI spans to their analyzers. Changing how a native span becomesa service, an endpoint or a relation, such as reading one more tag, is a Java change. The knowledge of what a span means
lives in two places, written in two styles.
This SWIP moves the whole native analysis into TAL rules hosted by the
trace-analyzermodule, and removes theAnalysisListenerFactorySPI: every analysis of a native span is a rule file that the configuration activates. It is aport and nothing else:
order, as the listeners do. They make the same stateful calls and side-effect calls in the same order, apart from the
one exception below, and write the same log lines, debug included, under the script's logger rather than the
listener's class name. OAL, storage, query, alarm and the UI are untouched, and so is every metric name.
segment on malformed input. A difference found while porting is a defect of the port, not a fix to make on the way.
A behaviour worth changing is changed afterwards, in its own change, with its own changelog line.
rule writes the sample as a
recordinbuild {}, where a native rule makes all of its outputs, so the sample istaken after the segment's last span. A segment that analyzes completely makes the same
samplecalls, with the samecontexts, in the same order, only later. A segment that fails no longer takes the samples of the GenAI spans before
the failure, see
Failure behaviour the port keeps.
TAL gains what the port needs:
sw-nativeinput type, provided through the existingTALSourceTypeProviderextension point,whose input is the span, with its segment as
span.segment.config: a read-only root:config.from(module, setting)reads any loaded module's setting, and a few namedentries read the values a module parses or changes at runtime.
build {}: a native rule makes its outputs in one deferred block.source {}hands an existing OALsource to the OAL engine, documented as the adoption path for the existing source system;
recordwrites what anoutput builder builds;
metrics {}declares a new metric, as it does in every TAL rule.log: what a per-span rule needs to reproduce alistener exactly, and to read like one.
contribute their own functions through a new
TALFunctionProviderextension point. Logicstays in the rules: a function reads a cache, a settings file or a lookup the rules cannot reach, or parses what TAL
has no syntax for.
The complete ported rule files are in the first comment of this discussion. A
test module runs the old listeners and the new rules side by side and compares their complete, ordered behaviour, the
exception's
samplecalls as a list of their own, following the precedent of the MAL and LAL move from Groovy to the v2compilers.
Scope
agent-analyzerand the modules plugged into ittal/RPCAnalysisListenernative-rpc.yaml: rulesnative-rpc-entry,native-rpc-exit,native-rpc-logic-endpointEndpointDepFromCrossThreadAnalysisListenernative-endpoint-cross-thread.yamlNetworkAddressAliasMappingListenernative-network-alias.yamlSegmentAnalysisListenernative-segment.yaml, plus theparseSegmentvalues the runner derives onspan.segmentbefore the spansVirtualServiceAnalysisListener:VirtualCacheProcessor,VirtualDatabaseProcessor,VirtualMQProcessornative-virtual-service.yaml: rulesnative-virtual-cache,native-virtual-database,native-virtual-mqVirtualServiceAnalysisListener:VirtualGenAIProcessor(only whengen-ai-analyzeris loaded)native-virtual-genai.yaml, loaded only whengen-ai-analyzeris loadedGenAIEvaluationAnalysisListener, plugged in byai-evaluationthrough theAnalysisListenerFactorySPInative-ai-evaluation.yaml, loaded only whenai-evaluationis loadedTraceAnalyzer,AnalysisListener,AnalysisListenerFactory, the listener interfaces,SegmentParserServiceImpland the listener managertrace-analyzerruns the rulesISegmentParserService, the segment entry point, inagent-analyzertrace-analyzer, whose runner implements itSix listeners and four processors move. The RPC listener becomes three rules because its
build()sends all entrysources, then all exit sources, then all logic-endpoint sources; three rules, flushed in file order, send in that
order. See How trace-analyzer runs the native rules.
Architecture Graph
trace-analyzeralready depends onagent-analyzer(oap-server/analyzer/trace-analyzer/pom.xml), so the runnerlives there with no new Maven dependency in either direction.
ISegmentParserServiceis internal: its only callers are the native trace receiver's gRPC and REST handlers, the Kafkafetcher's segment handler and the data-generator tool, all in this repository. It moves from
agent-analyzertotrace-analyzer.TraceAnalyzerModuledeclares it, and its provider registers the runner as its implementation. Thecallers add
trace-analyzerto their required modules and look the service up there, seeActivation and startup.
Proposed Changes
The
sw-nativeinput typeSwNativeTALSourceTypeProviderimplements the existingTALSourceTypeProviderextension point under the aliassw-native, besideotlp. Its input is the span. A rule reads it from two places, and each value is in exactly one:span: what the agent sent for the span:operationName,peer,spanId,parentSpanId,startTime,endTime,componentId,isError,skipAnalysis,spanLayer(the constant's name),isEntry,isExit,isLocal,tags(in wire order, each with
keyandvalue),refs(reference views withrefType,isCrossProcess,traceId,parentTraceSegmentId,parentSpanId,parentService,parentServiceInstance,parentEndpoint,networkAddressUsedAtPeer),ref(the first reference ornull),refsOrNullandsegment;span.segment: what the agent sent for the segment (service,serviceInstance,traceId,traceSegmentId,isSizeLimited,spans,bytes), and what the runner derives before the first rule runs, doingSegmentAnalysisListener.parseSegment's work with the same collaborators and log lines:startTime,endTime,latency,isError,tags(the searchable tags) andsampled.span.segmentis one read-only object per segment run, shared by every span view of it, asTraceAnalyzerhands everylistener the same
SegmentObject. A rule cannot change a span or the segment: theSegmentrecord stores the receivedsegment unchanged. Editing the stored trace is left to a proposal of its own.
What the listeners derive for one span alone, the rules write out where they need it:
identifyServiceLayer:FAASfor a span whosespanLayerisFAAS,GENERALfor any other;(span.endTime - span.startTime) as Integer, Java's(int)cast;SegmentAnalysisListener.build()runs once: asegmentDefguard on the first span.The
otlpinput'smetaroot and the statements that overwrite it are not part of thesw-nativeinput. The runnerruns the
sw-nativerules on every span of every segment; a rule'slayerkey isGENERAL.The
configrootconfigis a read-only root.config.from("<module>", "<setting>")reads a setting that a loaded module chooses toserve to TAL:
A module that lets TAL read its settings exposes a standard service, as it exposes any other: it lists
TALConfigReader(server-core) in itsModuleDefine.services(), and its provider registers an implementation overthe configuration it holds. The provider is never exposed, only the settings its reader serves.
The compiler takes the reader through
moduleManager.find(<module>).provider().getService(TALConfigReader.class).agent-analyzerservestraceAnalysisandmaxSlowSQLLength;ai-evaluationservessampleRateandactive, thedecision its
prepare()made from its judge configuration, and not the judge settings themselves. A rule file thatreads another module's settings lists it under
requiredModules.agent-analyzeris loaded whenever a native filecompiles, so a native file reads its settings without listing it.
A few named entries stay for values a rule looks up with an argument, values
agent-analyzerparses or changes atruntime, and the runner's own settings:
noUpstreamRealAddressAgents,slowDBAccessThreshold(type),slowCacheReadThreshold(type),slowCacheWriteThreshold(type),searchableTracesTags,segmentStatusAnalysisStrategy,forceSampleErrorSegmentandtraceSamplingPolicy, each read through the object thatowns it today.
gateways.ymlandserver-clusters.ymlstay behind the functionsisAddressConfiguredAsGatewayandclusterNameOrAddress.How trace-analyzer runs the native rules
The runner reproduces
TraceAnalyzer's lifecycle callback for callback. TAL stays per span. A listener's per-segmentbehaviour maps to two blocks and one kind of variable, each the counterpart of one Java mechanism:
TraceAnalyzertodaySegmentListener.parseSegment, before the spansspan.segmentvalues above, computed before the first rule's first runFirstAnalysisListener.parseFirst, on every span whose id is0, before any other callback on that span, whatever its typefirst {}of every rule, in file order, on every span whosespanIdis0, before any rule's regular run on that span, whatever its typefilterruns, in file order, on every span in wire order. A rule testsspan.isEntry,span.isExitorspan.isLocal; a span of an unknown type matches none, as no callback runs for it today, and the runner logs the same error for it under its own loggersegmentDef, a variable alive for one segmentbuild()of every listener, in listener order, after the last spanbuild {}blocks. A block is queued when a span's run reaches it. After the last span the runner runs the queued blocks rule by rule in file order, and within a rule in the order they were queuedNetworkAddressAliasMappingListener's alias row, sent during the span loopwriteNetworkAlias, sent at its statementGenAIEvaluationAnalysisListener's sample, taken during the span looprecordinbuild {}, taken in the build phase: the one place the port moves a send, see Failure behaviour the port keepslogstatements, under the script's loggerorg.apache.skywalking.trace.analysis.script.<script name>, and the functions that call the logging Java. The runner calls a compiled rule directly, not throughTalDSL.evaluate, so the[TAL] rule=...debug line the OTLP path writes before each rule is not written for a native ruleOrdering follows from this. A Java builder is one queued
build {}block: the parse callback fills it,build()formats its names and sends its sources. A rule that formats names inside
build {}therefore makes theNamingControlcalls, whose results depend onEndpointNameGrouping's per-service state and whose truncations log, atthe same point and in the same number as the Java does. A rule that sends from
build {}sends in the Java order.Failures follow the Java too. The runner catches nothing, neither around a rule nor around a record's
initandcomplete; the OTLP path catches around both. A rule that throws stops the segment at the same point a throwinglistener stops it today, and the exception reaches
ISegmentParserService.sendand the receiver unchanged:InvalidProtocolBufferException, so the exception leaves the handler.A throw before the build phase means no queued block runs, and so no evaluation sample of the segment is taken.
Everything already done stays done: alias rows sent, log lines written, and changes to the endpoint grouping's state. A
throw inside a
build {}block, including one raised while the OAL engine dispatches a source, stops the remainingblocks, and what was already sent stays sent.
The
sw-nativeinput has no retry dedup (dedupWindowdoes not apply), no network-alias pairing, and nopersist {}or
tag: the segment's storage is thenative-segmentrule'sSegmentrecord.prepareNetworkAlias,buildNetworkAlias,persist {},tag, theoutputTypekey andsink {}are compile errors in asw-nativerule,and so are
metrics {},source {}andrecordoutsidebuild {}: a native rule makes its outputs there.Activation and startup
tal/besideotlp-general.yaml; every native rule declaresinputType: sw-native. A missingfile or two native rules of one name fail the boot. A separate list keeps native analysis running for an operator who
set
SW_TRACE_TAL_FILESto their own OTLP files.requiredModulesat a file's top level compiles and runs its rules only when the modules are loaded:native-virtual-genailistsgen-ai-analyzer,native-ai-evaluationlistsai-evaluation, the checks the listenerfactories make today.
traceAnalysiskeeps its meaning: the RPC, cross-thread and alias rules abort when it is false.trace-analyzerdeclares among its required modules, when each is loaded,agent-analyzer, the moduleeach discovered
TALFunctionProvidernames, and every module a rule file lists underrequiredModules, so everymodule a rule reaches has started first. It registers the runner as
ISegmentParserServiceinprepare(), and bindsthe function providers and compiles the rules in
start(). The native trace receiver, the Kafka fetcher and thedata-generator tool require
trace-analyzerbesideagent-analyzer, so no segment reaches the runner before itsrules are compiled.
trace-analyzer. WithSW_TRACE_ANALYZER=-while the native trace receiver (on by default) or the Kafkafetcher is loaded, the module manager fails the boot, naming the module; with neither loaded, removing it still works
for OTLP alone. The data-generator tool's own
application.ymlgains atrace-analyzersection.agent-analyzer. The native trace receiver still requires it, as today.trace-analyzeralone, forOTLP, stays allowed and compiles no native file.
The order of
nativeTalFilesis part of the contract. No rule reads another rule's output directly: the onecross-file write, an alias row, reaches
native-rpc-exitonly through the alias cache, where it is not visible untilthe cache reloads, and a reload can land while a segment is still being analyzed, as it can today. Order still decides
what reaches an order-sensitive merge first (
EndpointRelationServerSideMetrics.combineandEndpointTraffic.combinekeepthe first object), what is already sent when a segment fails, and the order of the stateful naming calls through
EndpointNameGrouping. Insidenative-rpcthe rules are entry, exit and logic endpoint, the listener's build grouping;inside
native-virtual-service, cache, database and MQ, with GenAI after, the processor order.New TAL section: Source
This is the text proposed for the Source section of the native input's page,
tal-sw-native.md, seeDocumentation layout.
Other syntax additions
Assignment
<name> = <expr> [as <Type>], on every input.defdeclares a variable and=assigns it; today a laterdefof thesame name assigns, which reads like a second declaration, and it keeps working. A variable that the branches of an
ifassign is declared before them, with a typed
nullfor aString, aLayeror an enum (def owner = null as String)and a literal for a number or a boolean. The native rule files use
=for every assignment anddefonly to declare.foreach<list>is ajava.util.List, such asspan.refs,span.tags,span.segment.tagsor a list a function returns.The loop variable is typed to the list's element type when the rule compiles, from the getter's or function's generic
return type, so
ref.parentServiceandspanTag.keyare compile-checked chains. Adefinside the body declares avariable of one iteration. It is declared again at each iteration, and it is not visible after the loop: that is how
the Java builds a fresh builder per reference. A variable declared before the loop and assigned inside it with
=keepsits value across iterations.
foreachis allowed whereverifis. It replaces indexingsuch as
span.refs[0], which the compiler does not support on aList.buildsw-nativeonly, insideextractor {}. The block is queued when the run reaches it and runs once after the segment'slast span, see How trace-analyzer runs the native rules.
spanand its function context, as they were for that run, not the lastspan's. The
defvalues the run had when it reached the block, including the loop variables of theforeachit sitsin, captured by value. The
segmentDefvalues as they are when the block runs.caller = formatServiceName(caller)reads the copyand assigns it, as the Java formats a builder's field in place; the run and the other blocks do not see the change.
A
definside the block declares a variable of that block.sw-nativerule makes its outputs here and nowhere else, each at its statement:metrics {}, a new metric, as in every TAL rule. Itstimestampdefaults tospan.startTime, there being nometa, and a failure inside it skips that block, as it does everywhere: the first failure of a metric logs awarning, later ones log at debug. No shipped native rule
declares one;
source {}, one source of the existing OAL system;record, one record of an output builder.abort {}later in the same run withdraws the blocks that run queued. Anabort {}inside a blockstops that block only.
firstsw-nativeonly, at filter level. It runs on every span whosespanIdis0, before any rule's regular run on thatspan, whatever the span's type, as
FirstAnalysisListener.parseFirstdoes. It is a run of its own: it has its owndefvalues, anabort {}in it ends that run only, and it shares onlysegmentDefvalues with the rule's regularrun.
segmentDefsw-nativeonly. A variable of the rule that lives for one segment, as a listener's field does. It starts asnullforeach segment. The
segmentDefstatement declares it and assigns it each time it runs;<name> = <expr>assigns ittoo, anywhere in the rule, on any span of the segment; and the name reads it anywhere in the rule, before its first
assignment included. Its storage belongs to one run of the runner, never to the compiled
rule, so concurrent segments do not share it, and it is cleared when the segment ends, also when it fails.
writeNetworkAliassw-nativeonly. It sends, at its statement, aNetworkAddressAliasSetupwith:addressthe given address;representServiceformatServiceName(span.segment.service);representServiceNormaltrue;representServiceInstanceformatInstanceName(span.segment.serviceInstance);This is the row
NetworkAddressAliasMappingListenersends from an entry span's cross-process reference. It sends thesource and lets the dispatcher and the cache reload do the rest, without touching the cache. It does not use the OTLP
path's alias registration, which refuses an empty address or service and writes no instance.
The SkyWalking agent propagates the address the caller dialed in the
sw8header, so the callee alone has every factof the row, and nothing waits for a caller's half as the OTLP pairing does. The source has no
@ScopeDeclarationname, so
source {}cannot name it.logWrites the message through SLF4J under the logger
org.apache.skywalking.trace.analysis.script.<script name>, thescript being the rule file without its extension, such as
org.apache.skywalking.trace.analysis.script.native-rpc; thename is not configurable. Each
{}is replaced by the next argument as SLF4J formats it. The arguments are evaluatedonly when the level is enabled, as the Java's
isDebugEnabled()guards do. It is allowed wherever a statement is. Afunction that logs, such as
parseStatusCode, logs under the calling rule's script logger; a function that delegates toJava, such as the naming functions or
isAddressConfiguredAsGateway, logs as that Java logs today, under its own classname.
Typed Layer and enum values
asconverts a string to aLayeror to an enum a source or a record takes, such asRequestType,DetectPoint,MQOperation,VirtualCacheOperation,TagTypeandGenAITraceRefType:Layeris a registry, not a Java enum:as LayercallsLayer.valueOf(name), which fails on an unknown name, and aliteral is checked when the rule compiles against the layers registered by then, those a rule file's
layerDefinitionsregister included. An enum literal is checked against the enum's constants. Adefgiven a typedvalue has that type, so a misspelt name is a compile error where it is written, not a failure when its branch runs.
A
Layeror an enum value compared with a string literal compares by name, and the literal is checked when the rulecompiles:
calleeLayer != "FAAS",operation == "Write".Where each construct applies
The native constructs apply to
sw-nativerules only. The constructs the native input cannot support are refused insw-nativerules, and every other input type,otlpand any input aTALSourceTypeProvideradds, keeps them as itdoes today, so the restriction breaks no existing rule. The rules compile at boot, so a construct used under the wrong
inputTypefails the boot, naming the file, the rule and the construct, as a rule on an unregistered layer does today.A file that
requiredModulesleaves out is not compiled, and so not checked.sw-nativeonlybuild {},source {},record,first {},segmentDef,writeNetworkAlias, the functions for thesw-nativeinput, and the native views:spanas the native span,span.segmentand the reference and tag viewssw-nativemetaroot and the statements that overwrite it (service,instance,endpoint,layer), theoutputTypekey andsink {},tag,persist {},prepareNetworkAlias,buildNetworkAliasandpairing, and the functions that read data a native span does not carry or the pairing's rows:resource,scopeAttr,event,eventAttr,callerScopedAddress,resolveNetworkAlias,isGateway,findServerClusterName<name> = <expr>),foreach,log, theconfigroot, typedLayerand enum values,metrics {}(insidebuild {}in asw-nativerule),requiredModules,def,if,abort {},attrandhasAttr,urlHost,toJson,toJsonArray,component, and the functions for every inputNew keywords
source,record,foreach,in,build,first,segmentDef,writeNetworkAlias,logandconfig. They stillwork as field names and label keys, but no longer as
defnames. The shippedotlp-generalrule uses none of them as adefname. The existing keywords stay what they are: the native rules name their locals so that none is a keyword, suchas
spanTagrather thantagandlogicJsonrather thanvalue.recordsw-nativeonly, insidebuild {}. It writes one record of an output builder, aTALOutputBuilder, at itsstatement, as
source {}sends one OAL source.<Name>is the builder's short name or class name; each field statementcalls the builder's setter, with the conversions of
source {}. The runner creates a new builder, callsbindInput,sets the fields, then calls
initandcomplete, with nothing caught; onsw-nativethe metadata isnulland theinput is the native
spanview.TALOutputBuildergainsdefault boolean supportsInputType(String inputType),truefor every input but
sw-native, so existing builders such asSlowSQLkeep working where they do and are refused in anative rule. Any module can add a builder native rules write, by returning
trueforsw-nativeand listing it inMETA-INF/services. A native rule has nooutputTypekey and nosink {}: it decides withifandabort {}.New TAL functions
Every function below calls the Java the listeners call, or reproduces a private method of it where the table says so,
so a value, a failure and a log line are the same as theirs.
Functions for every input
buildServiceId(name, normal)IDManager.ServiceID.buildIdbuildInstanceId(serviceId, name)IDManager.ServiceInstanceID.buildIdbuildEndpointId(serviceId, name)IDManager.EndpointID.buildIdminuteTimeBucket(millis),recordTimeBucket(millis)TimeBucket.getMinuteTimeBucket,TimeBucket.getRecordTimeBucketround(double)longnearest the valueMath.round(double)cut(text, length)lengthcharactersStringUtil.cutisBlank(text)null, or empty once trimmedStringUtil.isBlankemptyList(),emptyMap()ArrayListorHashMapEndpointSourceBuilderstarts withparseJsonObject(text)nullfor blank text; malformed or non-object text fails the rule. The getters that follow are Gson's, so their coercions and failures are Gson's.toJson()answers an empty object insteadGson.fromJson(text, JsonObject.class)parseLongOrZero(text),parseIntOrZero(text)0fornullor empty text; the number; or0after the warningFailed to parse value to long: {}(to intfor the second) under the script's logger, for text that does not parseGenAIMeterAnalyzer's privateparseSafeLongandparseSafeInt:Long.parseLong,Integer.parseIntmurmur3Hash(text)longHashing.murmur3_128().hashString(text, UTF_8).asLong(), asDefaultAIEvaluationSamplingPolicyhashes a trace idfloorMod(x, y)longvalues, never negative for a positiveyMath.floorMod(long, long)Functions for the
sw-nativeinputContributed by
trace-analyzer, which reachesagent-analyzer's configuration files and the core's caches. They readthe run's span where they need it. The naming functions are here and not for every input: an
otlprule names itsentities through
metrics {}, whose entitiesMeterEntityalready formats, so only the native rules, which must makethe listeners' exact
NamingControlcalls, need to call them.formatServiceName(name)nullstaysnull; a truncation logsNamingControl.formatServiceNameformatInstanceName(name)NamingControl.formatInstanceNameformatEndpointName(service, endpoint)NamingControl.formatEndpointNamematchGenAIModel(model)gen-ai-config.yml:provider,unknownwhen none or when the model is empty, andpricing, the matched model'sinputEstimatedCostPerMandoutputEstimatedCostPerM(double),nullwhen no model matches. The first call loadsgen-ai-config.ymlGenAIModelMatcher.getInstance().match(model)(library-util), whichGenAIContextResolver.resolveasks for the provider andGenAIProviderPrefixMatcher.matchfor the pricestagList()key.trim() + ":" + value.trim()strings, in wire order, in a new listEndpointSourceBuilder.setTag, thetagslisttagMapKeepingLast()EndpointSourceBuilder.setTag, theoriginalTagsmaptagMapKeepingFirst()toMapwith(left, right) -> leftinGenAIEvaluationAnalysisListenerandGenAIMeterAnalyzertagMapRejectingRepeats()Collectors.toMapthrowsCollectors.toMapwithout a merge function inVirtualCacheProcessorparseStatusCode(text)Integer, ornullafter logging, under the script's logger, the warningspan {} has illegal status code {}with the wire span and the textInteger.parseIntinsetPublicAttrs, itscatch (NumberFormatException)andlog.warnclusterNameOrAddress(address)server-clusters.ymllists the address under, or the address; nothing caught. The existingfindServerClusterNameanswers empty instead, and catches its exceptionsServerClusterConfig.clusterNameOrAddressfindNetworkAlias(address)null. Itsserviceandinstanceare decoded from its ids, the service first and both always, so a malformed id fails even when the instance is not used.instanceis_blankwhen the row names noneNetworkAddressAliasCache.get, and theIDManagerdecoding inRPCAnalysisListener.parseExitisAddressConfiguredAsGateway(address)gateways.ymllists the address, matched exactly, with that class's debug line; the existingisGatewayalso retries a bare host at port 80, which the agent path does notUninstrumentedGatewaysConfig.isAddressConfiguredAsGatewayContributed by other modules, and the extension points
The GenAI extraction and the evaluation's sampling policy are logic, so the rules write them out: the tokens, the time
to first token and the cost from the model's prices in
native-virtual-genai, and innative-ai-evaluationthedecision from
ai-evaluation'sactiveandsampleRate, read throughconfig.from, and a murmur3 hash of the traceid. The OTLP and Zipkin paths keep calling the same Java, which reads the same settings.
ai-evaluationcontributesGenAIEvaluationSample(
org.apache.skywalking.oap.server.ai.evaluation.listener.GenAIEvaluationSampleBuilder), aTALOutputBuilderthatnative-ai-evaluationwrites withrecord. Itscompletebuilds theAIEvaluationContextthe listener builds andcalls
IAIEvaluationService.sample, which queues it for the LLM judge: a hand-off, not a stored record.TALFunctionProvider(server-core, found throughServiceLoader, bound intrace-analyzer'sstart()) lets amodule contribute functions without depending on
trace-analyzer. The shipped rules calltrace-analyzer's own.TALConfigReader(server-core) is the module service behindconfig.from, see Theconfigroot.TALOutputBuilder.supportsInputTypesays which inputs may write a builder, every input butsw-nativeby default.Failure behaviour the port keeps
These Java paths fail a segment on input the agent should not send. The port reproduces each, through the functions
above and the runner's no-catch rule:
VirtualMQProcessortransmission.latencytag thatLong.parseLongrejects, even when a later one is validspanTag.value as Longin an assignment, which callsLong.parseLong, after the same blank checkRPCAnalysisListenerlogic endpointx-letag that is blank (Gson answersnull, thenhasthrows), not a JSON object, malformed, or holds a value its getter cannot convertparseJsonObjectand the same Gson getters, in the same order; a tag without the keys is skipped, as todayVirtualCacheProcessorcache.typeguardtagMapRejectingRepeats(), called firstbuild()transmission.latencytag converted bycore.oal's(str->long)castbuild {}orderMaking any of them tolerant is a behaviour change, and so it is out of scope.
One failure path changes, by choice: when the evaluation sample is taken.
GenAIEvaluationAnalysisListenersamples aGenAI span during the span loop.
native-ai-evaluationwrites the sample as arecordinbuild {}, wherea
sw-nativerule makes its outputs, so it is taken in the build phase, after every earlier file's blocks. Thesampling decision and the tag reads stay in the span run, in their place.
samplecalls, with the same contexts, in the same order. Thesampling decision is a hash of the trace id, and the evaluation queue shares no state with the OAL engine, so the
analysis itself does not change. What the evaluation service does with a call depends on its consumer threads, today
as after the port: whether a task of the same id is still pending, whether the bounded queue has room, and in which
minute its counters move, which the OAP's self-observability rules read as per-minute increases. A call made later
can meet a different state, so a duplicate task, such as one from a re-sent segment, or a call that meets a full
queue, can be accepted or skipped differently, and a counter can move in the next minute.
failure have theirs taken.
sampleitself checks the context and counts a full queue rather than throwing, so taking the samples laststops nothing that would have run.
Verifying that the rules behave as the listeners did
When MAL, LAL and the hierarchy rules moved from Groovy to the ANTLR4 and Javassist compilers (#13723), the Groovy
runtime stayed as test-only modules and v1-v2 checkers compared both engines on the same scripts, until #13782 removed
them after the new engines shipped. This SWIP repeats that pattern with a stricter comparison: exact, ordered, and
covering stateful calls, side effects and log lines.
test/script-cases/trace-analysis-listener-v1holds the removed Java, moved withgit mvand neverpackaged:
TraceAnalyzer,SegmentParserServiceImpl, the listener manager and interfaces, the six listeners, the fourprocessors, their builders and the listener assembly frozen at the base commit.
test/script-cases/trace-analysis-v1-v2-checkerholds
NativeTraceComparisonTest, the expected-data generator andNativeTraceBenchmark.produces one ordered event log per segment: every source sent, rendered field by field by reflection; every
NamingControlcall; every sampler, searchable-tags, status-analyzer and alias-cache call; every log line, with thelistener's class logger and the script's logger counted as one; and every exception. The two logs must be equal,
event by event, and so must the
EndpointNameGroupingstates after each segment. The evaluationsamplecalls, whichthe port moves to the build phase, are compared as their own list. A second level runs the fixtures through a real
DispatcherManagerwith the shipped OAL scripts and compares what reaches the stream processors and what the mergesand TopN buffers keep.
Node.js, PHP, Kafka, virtual MQ, server clusters, meter, virtual GenAI, mixed GenAI), fixtures built from them for every
branch and malformed input, several configuration profiles, and cold and warm alias-cache passes.
JaCoCo branch coverage over the moved classes.
.data.yamlholds the v1 event logs, written by a separate generator.A JMH benchmark compares both sides, and a regression blocks the switch. Boot tests cover the module combinations, and
every listed e2e case runs unchanged.
Steps
source {},record,foreach,build {},first {},segmentDef,writeNetworkAlias,log, theconfigroot andTALConfigReader, typed values, file-levelrequiredModules,TALFunctionProvider,supportsInputTypeand the new functions, with the compiler work they need: getters evaluatedwhere they stand, setters for objects, enums,
Layer, lists and maps, deferredbuild {}code, per-segmentsegmentDefstorage, input-aware compile checks, ordered rule loading and no catch on the native path.sw-nativeinput and its views, the runner,ISegmentParserServicemoved totrace-analyzer,nativeTalFiles, the shipped rule files, the functions andGenAIEvaluationSample; the listeners and theAnalysisListenerFactorySPI move to the test module, and the checker lands. It lands only with the comparisonpassing on every fixture and profile.
Imported Dependencies libs and their licenses.
None in the distribution. The checker module uses the JaCoCo Maven plugin (EPL-2.0) at build time only; it is not
packaged.
Compatibility
the same fields in the same order, the same
NamingControlcalls, sampler calls, alias rows, log lines and failures.The evaluation
samplecalls are the same, with the same contexts and in the same order, for a segment thatcompletes, though made after its last span, so what the evaluation service accepts can differ with its consumers'
timing; a segment that fails makes none, see Failure behaviour the port keeps.
trace-analyzer.default.nativeTalFiles.talFiles,traceAnalysis, the analyzer's thresholds and every other existing setting keep their meaning.SW_TRACE_ANALYZER=-now stops the boot while the native trace receiver, which is on by default, or the Kafkafetcher is loaded, since
trace-analyzerhosts the native analysis. An operator who removed the module to turnoff OTLP analysis sets
talFilesto empty instead, which the module already supports. With neither callerloaded, removing the module still works.
AnalysisListenerFactorySPI is removed. A third-party listener moves to a rule file, plusa
TALFunctionProviderfor what TAL cannot read or call, and aTALOutputBuilderthat supportssw-nativeforwhat it hands over that is not an OAL source.
TALOutputBuildergainssupportsInputTypeas a default method, soexisting implementations compile and behave unchanged. The module system does not change: a module that lets TAL
read its settings declares the
TALConfigReaderservice, and its provider registers it, asagent-analyzerandai-evaluationdo.defnames. Plainassignment is additive too: a later
defof a declared name keeps assigning it.that a hoisted getter dereferenced before its guard threw there, which on the OTLP path skips the rule for that span,
with a warning the first time and at debug after; it now reaches the guard. The
otlpinput's chains read protobufgetters, which never return
nulland have no side effects, so the shippedotlp-generalrules behave the same. Acustom input whose getters can return
nullmay now pass a guard where it used to fail before it.loggers to
org.apache.skywalking.trace.analysis.script.<script name>and the runner's own logger, so a loggingconfiguration that sets a level for a listener class sets it for the script's logger instead.
GenAIMeterAnalyzer'slines for native spans, its missing-model debug line and its parse warnings, move to the
native-virtual-genaiscript's logger; it keeps logging under its own name for OTLP and Zipkin spans.
nativeTalFiles, changes the nativeanalysis, as with any rule file. The shipped files and the default order are what the checker compares.
new constructs, the function providers and the
TALConfigReaderservicesconfig.fromreads before it ships them.General usage docs
Documentation layout
The tracing documentation is organized by where a trace comes from. TAL comes first, as the language the native and
OpenTelemetry analyses are written in. Then each source has the same parts: its setup, its bundled analysis, its TAL
input reference, and best practices where there are some. Every existing page keeps its path; sections move only out of
today's
tal.md, to the OTLP input page, and the Zipkin part oftrace-sampling.md, to the Zipkin page.The comparison table on the language page:
otlpsw-nativetalFiles(SW_TRACE_TAL_FILES)nativeTalFiles(SW_TRACE_NATIVE_TAL_FILES)service.layerrequiredModulesare loaded, in list order, on every span of the segment, in wire orderspan, the OTLPSpan, andmeta, what the receiver derived from itspan, the native span, andspan.segment, its segmentfirst {}on a span0, the span's run, andbuild {}after the segment's last span;segmentDeflives for the segmentmetrics {}; a record throughoutputTypeandsink {};persist {}andtagfor the stored spanbuild {}:metrics {},source {}for OAL sources,recordfor an output builderpersist {}votes to drop it, then past the receiver's rate limit and head sampling. Anenforcer {}skips head sampling only; withotlpTraceStorage: zipkinit marks the spandebug, which the Zipkin path keeps past both its rate limit and its sample rate. Neither overrides a vetonative-segmentrule'sSegmentsource when the segment is sampled; no rule changes itprepareNetworkAliasandbuildNetworkAliaswriteNetworkAlias, from the address thesw8header carriesmetrics {}entity constructorsformatServiceName,formatInstanceNameandformatEndpointName, where the listeners called the naming rulesdedupWindowabove 0, a span sent again to the same node within the window, while the node still holds its id, is analyzed onceAll reactions