├── web ├── .prettierignore ├── public │ ├── favicon.ico │ └── index.html ├── .prettierrc.json ├── eslint.config.mjs ├── rollup.config.js ├── src │ ├── main.js │ ├── components │ │ ├── utils │ │ │ ├── escape-html.js │ │ │ ├── json-payload.js │ │ │ └── base64.js │ │ ├── panels │ │ │ ├── styles.js │ │ │ ├── config-panel.js │ │ │ ├── payload-panel.js │ │ │ ├── result-panel.js │ │ │ └── result-panel.styles.js │ │ ├── playground.styles.js │ │ ├── controls │ │ │ ├── copy-link-button.js │ │ │ └── index.js │ │ ├── user-consent-banner │ │ │ └── index.js │ │ ├── navbar │ │ │ └── index.js │ │ ├── examples.js │ │ └── playground.js │ ├── styles.js │ └── wasm_exec.js └── package.json ├── NOTICE.txt ├── docker └── nginx │ └── default.conf ├── testdata ├── transformprocessor.yaml ├── filterprocessor.yaml ├── traces.json ├── profiles.json ├── logs.json └── metrics.json ├── .github └── workflows │ ├── ci.yml │ └── build.yml ├── Dockerfile ├── internal ├── versions.go ├── transformprocessorexecutor_test.go ├── filterprocessorexecutor_test.go ├── filterprocessorexecutor.go ├── transformprocessorexecutor.go ├── executor.go ├── log_observer.go ├── processorexecutor_test.go └── processorexecutor.go ├── main.go ├── README.md ├── wasm ├── main.go └── internal │ ├── ottlplayground.go │ └── ottlplayground_test.go ├── Makefile ├── go.mod ├── LICENSE.txt └── ci-tools └── main.go /web/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | rollup.config.js 3 | **/wasm_exec.js 4 | **/bundle.js -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/ottl-playground/HEAD/web/public/favicon.ico -------------------------------------------------------------------------------- /web/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "bracketSpacing": false, 6 | "arrowParens": "always" 7 | } 8 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | OTTL Playground 2 | Copyright 2012-2018 Elasticsearch B.V. 3 | 4 | This product includes software developed by The Apache Software Foundation (http://www.apache.org/). -------------------------------------------------------------------------------- /docker/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | location ~ ^/wasm/.*\.wasm$ { 3 | add_header Content-Encoding br; 4 | add_header Vary Accept-Encoding; 5 | 6 | types { application/wasm br; } 7 | try_files $uri.br =404; 8 | } 9 | 10 | listen 8080; 11 | root /usr/share/nginx/html; 12 | } -------------------------------------------------------------------------------- /web/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | 4 | 5 | export default [ 6 | { 7 | languageOptions: {globals: globals.browser}, 8 | }, 9 | { 10 | ignores: [ 11 | "node_modules/*", 12 | "rollup.config.js", 13 | "**/wasm_exec.js", 14 | "**/bundle.js", 15 | ] 16 | }, 17 | pluginJs.configs.recommended, 18 | ]; -------------------------------------------------------------------------------- /testdata/transformprocessor.yaml: -------------------------------------------------------------------------------- 1 | trace_statements: 2 | - context: resource 3 | statements: 4 | - set(attributes["service.new_name"], attributes["service.name"]) 5 | - delete_key(attributes, "service.name") 6 | log_statements: 7 | - context: log 8 | statements: 9 | - set(attributes["body"], body) 10 | metric_statements: 11 | - context: resource 12 | statements: 13 | - set(attributes["foo"], "bar") 14 | profile_statements: 15 | - context: profile 16 | statements: 17 | - set(attributes["foo"], "bar") -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Setup Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: '1.22.x' 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: '20.x' 22 | - name: Install Go dependencies 23 | run: go get . 24 | - name: Build 25 | run: make build-web build-wasm 26 | - name: Run Go Tests 27 | run: go test -v ./... 28 | -------------------------------------------------------------------------------- /testdata/filterprocessor.yaml: -------------------------------------------------------------------------------- 1 | traces: 2 | span: 3 | - 'attributes["container.name"] == "app_container_1"' 4 | - 'resource.attributes["host.name"] == "localhost"' 5 | - 'name == "app_3"' 6 | spanevent: 7 | - 'attributes["grpc"] == true' 8 | - 'IsMatch(name, ".*grpc.*")' 9 | metrics: 10 | metric: 11 | - 'name == "my.metric" and resource.attributes["my_label"] == "abc123"' 12 | - 'type == METRIC_DATA_TYPE_HISTOGRAM' 13 | datapoint: 14 | - 'metric.type == METRIC_DATA_TYPE_SUMMARY' 15 | - 'resource.attributes["service.name"] == "my_service_name"' 16 | logs: 17 | log_record: 18 | - 'IsMatch(body, ".*password.*")' 19 | - 'severity_number < SEVERITY_NUMBER_WARN' -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Frontend 2 | FROM node:lts AS webbuilder 3 | WORKDIR /web 4 | COPY web . 5 | COPY Makefile . 6 | RUN make build-web 7 | 8 | # Web-assembly and server 9 | FROM golang:1.24 AS wasmbuilder 10 | WORKDIR /build 11 | COPY ./ . 12 | RUN make build-wasm 13 | ARG SKIP_BUILD_UNREGISTERED_VERSIONS 14 | RUN if [ "$SKIP_BUILD_UNREGISTERED_VERSIONS" = "" ]; then make build-unregistered-versions ; fi 15 | 16 | # NGINX with brotli 17 | FROM alpine 18 | RUN apk add brotli nginx 19 | COPY docker/nginx/default.conf /etc/nginx/http.d/default.conf 20 | COPY --from=webbuilder /web/public /usr/share/nginx/html 21 | COPY --from=wasmbuilder /build/web/public/wasm /usr/share/nginx/html/wasm 22 | RUN for file in /usr/share/nginx/html/wasm/ottlplayground-*.wasm ; do brotli -9j "$file" ; done 23 | CMD ["nginx", "-g", "daemon off;"] 24 | EXPOSE 8080 -------------------------------------------------------------------------------- /web/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import {terser} from 'rollup-plugin-terser'; 4 | import * as path from 'path'; 5 | 6 | // `npm run build` -> `production` is true 7 | // `npm run dev` -> `production` is false 8 | const production = !process.env.ROLLUP_WATCH; 9 | const webOutputDir = path.join( 10 | process.env.WEB_OUTPUT_DIR || 'public', 11 | 'bundle.js' 12 | ); 13 | 14 | export default { 15 | input: 'src/main.js', 16 | output: { 17 | file: webOutputDir, 18 | format: 'iife', // immediately-invoked function expression — suitable for 12 | 13 | OTTL Playground 14 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /web/src/components/utils/json-payload.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | export const getJsonPayloadType = (payload) => { 21 | let json = JSON.parse(payload); 22 | if (json['resourceLogs']) { 23 | return 'logs'; 24 | } else if (json['resourceSpans']) { 25 | return 'traces'; 26 | } else if (json['resourceMetrics']) { 27 | return 'metrics'; 28 | } else if (json['resourceProfiles']) { 29 | return 'profiles'; 30 | } else { 31 | throw new Error( 32 | 'document must include an OTLP ["resourceLogs", "resourceSpans", "resourceMetrics", "resourceProfiles"] root element' 33 | ); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /web/src/components/utils/base64.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | export function utf8ToBase64(str) { 21 | const encoder = new TextEncoder(); 22 | const utf8Array = encoder.encode(str); 23 | let binaryStr = ''; 24 | utf8Array.forEach((byte) => { 25 | binaryStr += String.fromCharCode(byte); 26 | }); 27 | return btoa(binaryStr); 28 | } 29 | 30 | export function base64ToUtf8(str) { 31 | const binaryStr = atob(str); 32 | const binaryLen = binaryStr.length; 33 | const utf8Array = new Uint8Array(binaryLen); 34 | for (let i = 0; i < binaryLen; i++) { 35 | utf8Array[i] = binaryStr.charCodeAt(i); 36 | } 37 | const decoder = new TextDecoder(); 38 | return decoder.decode(utf8Array); 39 | } 40 | -------------------------------------------------------------------------------- /internal/transformprocessorexecutor_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package internal 21 | 22 | import ( 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | const ( 30 | transformprocessorConfig = "transformprocessor.yaml" 31 | ) 32 | 33 | func Test_TransformProcessorExecutor_ParseConfig(t *testing.T) { 34 | yamlConfig := readTestData(t, transformprocessorConfig) 35 | executor := NewTransformProcessorExecutor().(*transformProcessorExecutor) 36 | assert.NotNil(t, executor) 37 | parsedConfig, err := executor.parseConfig(yamlConfig) 38 | require.NoError(t, err) 39 | 40 | require.NotNil(t, parsedConfig) 41 | require.NotEmpty(t, parsedConfig.ErrorMode) 42 | require.NotEmpty(t, parsedConfig.TraceStatements) 43 | require.NotEmpty(t, parsedConfig.MetricStatements) 44 | require.NotEmpty(t, parsedConfig.MetricStatements) 45 | } 46 | -------------------------------------------------------------------------------- /internal/filterprocessorexecutor_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package internal 21 | 22 | import ( 23 | "os" 24 | "path/filepath" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/stretchr/testify/require" 29 | ) 30 | 31 | const ( 32 | filterprocessorConfig = "filterprocessor.yaml" 33 | ) 34 | 35 | func Test_FilterProcessorExecutor_ParseConfig(t *testing.T) { 36 | yamlConfig, err := os.ReadFile(filepath.Join("..", "testdata", filterprocessorConfig)) 37 | require.NoError(t, err) 38 | 39 | executor := NewFilterProcessorExecutor().(*filterProcessorExecutor) 40 | assert.NotNil(t, executor) 41 | 42 | parsedConfig, err := executor.parseConfig(string(yamlConfig)) 43 | require.NoError(t, err) 44 | require.NotNil(t, parsedConfig) 45 | require.NotEmpty(t, parsedConfig.ErrorMode) 46 | require.NotEmpty(t, parsedConfig.Logs) 47 | require.NotEmpty(t, parsedConfig.Traces) 48 | require.NotEmpty(t, parsedConfig.Metrics) 49 | } 50 | -------------------------------------------------------------------------------- /internal/filterprocessorexecutor.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package internal 21 | 22 | import ( 23 | "github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor" 24 | ) 25 | 26 | type filterProcessorExecutor struct { 27 | *processorExecutor[filterprocessor.Config] 28 | } 29 | 30 | func (f filterProcessorExecutor) Metadata() Metadata { 31 | return newMetadata( 32 | "filter_processor", 33 | "Filter processor", 34 | "github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor", 35 | "https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/filterprocessor", 36 | ) 37 | } 38 | 39 | // NewFilterProcessorExecutor creates an internal.Executor that runs OTTL statements using 40 | // the [filterprocessor]. 41 | func NewFilterProcessorExecutor() Executor { 42 | executor := newProcessorExecutor[filterprocessor.Config](filterprocessor.NewFactory()) 43 | return &filterProcessorExecutor{executor} 44 | } 45 | -------------------------------------------------------------------------------- /internal/transformprocessorexecutor.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package internal 21 | 22 | import ( 23 | "github.com/open-telemetry/opentelemetry-collector-contrib/processor/transformprocessor" 24 | ) 25 | 26 | type transformProcessorExecutor struct { 27 | *processorExecutor[transformprocessor.Config] 28 | } 29 | 30 | func (t transformProcessorExecutor) Metadata() Metadata { 31 | return newMetadata( 32 | "transform_processor", 33 | "Transform processor", 34 | "https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/transformprocessor", 35 | "https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/transformprocessor", 36 | ) 37 | } 38 | 39 | // NewTransformProcessorExecutor creates an internal.Executor that runs OTTL statements using 40 | // the [transformprocessor]. 41 | func NewTransformProcessorExecutor() Executor { 42 | executor := newProcessorExecutor[transformprocessor.Config](transformprocessor.NewFactory()) 43 | return &transformProcessorExecutor{executor} 44 | } 45 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "log" 24 | "net/http" 25 | "os" 26 | "path/filepath" 27 | "time" 28 | ) 29 | 30 | const ( 31 | relativeWebPublicDir = "web/public" 32 | defaultAddr = ":8080" 33 | ) 34 | 35 | func main() { 36 | listenAddress, ok := os.LookupEnv("ADDR") 37 | if !ok { 38 | listenAddress = defaultAddr 39 | } 40 | 41 | mux := http.NewServeMux() 42 | mux.HandleFunc("/", http.FileServer(http.Dir(webPublicDir())).ServeHTTP) 43 | 44 | server := &http.Server{ 45 | Addr: listenAddress, 46 | ReadHeaderTimeout: 20 * time.Second, 47 | Handler: mux, 48 | } 49 | 50 | log.Println("Listening on ", listenAddress) 51 | 52 | err := server.ListenAndServe() 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | } 57 | 58 | func webPublicDir() string { 59 | if _, err := os.Stat(relativeWebPublicDir); err == nil { 60 | return relativeWebPublicDir 61 | } 62 | executable, err := os.Executable() 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | return filepath.Join(filepath.Dir(executable), relativeWebPublicDir) 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OTTL Playground 2 | 3 | The OTTL Playground is a powerful and user-friendly tool designed to allow users to experiment with OTTL effortlessly. 4 | The playground provides a rich interface for users to create, modify, and test statements in real-time, making it easier 5 | to understand how different configurations impact the OTLP data transformation. 6 | 7 | ### Building 8 | 9 | **Requirements:** 10 | - Go 1.22 (https://go.dev/doc/install) 11 | - Node.js (https://nodejs.org/en/download/prebuilt-installer) 12 | 13 | By default, the built resources are placed into the `web/public` directory. After successfully 14 | compiling the WebAssembly and Frontend, this directory is ready to be deployed as a static website. 15 | Given that the WebAssembly size is relatively big, it's highly recommended to serve it using a compression 16 | method, such as `gzip` or `brotli`. 17 | 18 | ```shell 19 | make build 20 | ``` 21 | 22 | ##### Developing 23 | 24 | The `web` directory contains the frontend source code, and uses `npm` as package manager. 25 | To start the local development server: 26 | 27 | ```shell 28 | npm run serve public 29 | ``` 30 | 31 | Automatic reload the code changes: 32 | 33 | ```shell 34 | npm run watch 35 | ``` 36 | 37 | ### Running 38 | 39 | #### Local 40 | 41 | For **testing** purpose only, after successfully building the project resources, it can be run by 42 | using the `main.go` server implementation. 43 | 44 | To improve the load performance and saving bandwidth in real deployments, 45 | please confider hosting it using a server with compressing capabilities, such as `gzip` or `brotli`. 46 | 47 | ``` 48 | go run main.go 49 | ``` 50 | 51 | #### Docker 52 | 53 | The Docker image delivered with this project serves the static website using Nginx (port 8080), and 54 | applying static `brotli` compression to the WebAssemblies files. 55 | 56 | ```shell 57 | docker build . -t ottlplayground 58 | ``` 59 | 60 | ```shell 61 | docker run -d -p 8080:8080 ottlplayground 62 | ``` 63 | -------------------------------------------------------------------------------- /testdata/traces.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceSpans": [ 3 | { 4 | "resource": { 5 | "attributes": [ 6 | { 7 | "key": "service.name", 8 | "value": { 9 | "stringValue": "my.service" 10 | } 11 | } 12 | ] 13 | }, 14 | "scopeSpans": [ 15 | { 16 | "scope": { 17 | "name": "my.library", 18 | "version": "1.0.0", 19 | "attributes": [ 20 | { 21 | "key": "my.scope.attribute", 22 | "value": { 23 | "stringValue": "some scope attribute" 24 | } 25 | } 26 | ] 27 | }, 28 | "spans": [ 29 | { 30 | "traceId": "5b8efff798038103d269b633813fc60c", 31 | "spanId": "eee19b7ec3c1b174", 32 | "parentSpanId": "eee19b7ec3c1b173", 33 | "name": "I'm a server span", 34 | "startTimeUnixNano": "1544712660000000000", 35 | "endTimeUnixNano": "1544712661000000000", 36 | "kind": 2, 37 | "attributes": [ 38 | { 39 | "key": "my.span.attr", 40 | "value": { 41 | "stringValue": "some value" 42 | } 43 | } 44 | ] 45 | }, 46 | { 47 | "traceId": "5b8efff798038103d269b633813fc60c", 48 | "spanId": "eee19b7ec3c1b173", 49 | "parentSpanId": "eee19b7ec3c1b173", 50 | "name": "Me too", 51 | "startTimeUnixNano": "1544712660000000000", 52 | "endTimeUnixNano": "1544712661000000000", 53 | "kind": 1, 54 | "attributes": [ 55 | { 56 | "key": "my.span.attr", 57 | "value": { 58 | "stringValue": "some value" 59 | } 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /wasm/main.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | 3 | /* 4 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 5 | * or more contributor license agreements. See the NOTICE file distributed with 6 | * this work for additional information regarding copyright 7 | * ownership. Elasticsearch B.V. licenses this file to you under 8 | * the Apache License, Version 2.0 (the "License"); you may 9 | * not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, 15 | * software distributed under the License is distributed on an 16 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | * KIND, either express or implied. See the License for the 18 | * specific language governing permissions and limitations 19 | * under the License. 20 | */ 21 | 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | 27 | "syscall/js" 28 | 29 | "github.com/elastic/ottl-playground/wasm/internal" 30 | ) 31 | 32 | func handlePanic() { 33 | if r := recover(); r != nil { 34 | fmt.Println("recovered from", r) 35 | } 36 | } 37 | 38 | func executeStatementsWrapper() js.Func { 39 | return js.FuncOf(func(_ js.Value, args []js.Value) any { 40 | defer handlePanic() 41 | if len(args) != 4 { 42 | return internal.NewErrorResult("invalid number of arguments", "") 43 | } 44 | 45 | config := args[0].String() 46 | ottlDataType := args[1].String() 47 | ottlDataPayload := args[2].String() 48 | executorName := args[3].String() 49 | return js.ValueOf(internal.ExecuteStatements(config, ottlDataType, ottlDataPayload, executorName)) 50 | }) 51 | } 52 | 53 | func getEvaluatorsWrapper() js.Func { 54 | return js.FuncOf(func(_ js.Value, _ []js.Value) any { 55 | defer handlePanic() 56 | return js.ValueOf(internal.StatementsExecutors()) 57 | }) 58 | } 59 | 60 | func main() { 61 | js.Global().Set("executeStatements", executeStatementsWrapper()) 62 | js.Global().Set("statementsExecutors", getEvaluatorsWrapper()) 63 | <-make(chan struct{}) 64 | } 65 | -------------------------------------------------------------------------------- /web/src/components/panels/styles.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import {css} from 'lit-element'; 21 | import {globalStyles} from '../../styles'; 22 | 23 | const codePanelsStyle = css` 24 | .code-panel-parent { 25 | display: flex; 26 | flex-flow: column; 27 | height: 100%; 28 | } 29 | 30 | .code-panel-controls { 31 | flex-wrap: nowrap; 32 | border-left: #f5a800 4px solid; 33 | border-top: #eee 1px solid; 34 | border-bottom: #eee 1px solid; 35 | width: 99%; 36 | overflow-x: clip; 37 | } 38 | 39 | .code-panel-controls div { 40 | float: left; 41 | text-align: center; 42 | margin: 10px 5px 10px 5px; 43 | text-decoration: none; 44 | font-size: 17px; 45 | } 46 | 47 | .code-panel-controls div.right { 48 | float: right; 49 | overflow: hidden; 50 | align-items: center; 51 | 52 | div:not(:last-child) { 53 | margin-right: 4px; 54 | } 55 | } 56 | 57 | .code-panel-controls-header { 58 | width: 50%; 59 | overflow: hidden; 60 | } 61 | 62 | .code-editor-container { 63 | display: flex; 64 | height: 100%; 65 | overflow: auto; 66 | } 67 | 68 | .code-editor-container .wrapper { 69 | width: 100%; 70 | } 71 | 72 | .cm-editor { 73 | height: calc(100%); 74 | } 75 | 76 | .cm-scroller { 77 | overflow: auto; 78 | } 79 | `; 80 | 81 | export const codePanelsStyles = [codePanelsStyle, globalStyles]; 82 | -------------------------------------------------------------------------------- /internal/executor.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package internal 21 | 22 | type Metadata struct { 23 | ID string 24 | Name string 25 | Path string 26 | Version string 27 | DocsURL string 28 | } 29 | 30 | func newMetadata(id, name, path, docsURL string) Metadata { 31 | return Metadata{ 32 | ID: id, 33 | Name: name, 34 | Path: path, 35 | DocsURL: docsURL, 36 | Version: CollectorContribProcessorsVersion, 37 | } 38 | } 39 | 40 | // Executor evaluates OTTL statements using specific configurations and inputs. 41 | type Executor interface { 42 | // ExecuteLogStatements evaluates log statements using the given configuration and JSON payload. 43 | // The returned value must be a valid plog.Logs JSON representing the input transformation. 44 | ExecuteLogStatements(config, input string) ([]byte, error) 45 | // ExecuteTraceStatements is like ExecuteLogStatements, but for traces. 46 | ExecuteTraceStatements(config, input string) ([]byte, error) 47 | // ExecuteMetricStatements is like ExecuteLogStatements, but for metrics. 48 | ExecuteMetricStatements(config, input string) ([]byte, error) 49 | // ExecuteProfileStatements is like ExecuteLogStatements, but for profiles. 50 | ExecuteProfileStatements(config, input string) ([]byte, error) 51 | // ObservedLogs returns the statements execution's logs 52 | ObservedLogs() *ObservedLogs 53 | // Metadata returns information about the executor 54 | Metadata() Metadata 55 | } 56 | 57 | func Executors() []Executor { 58 | return []Executor{ 59 | NewTransformProcessorExecutor(), 60 | NewFilterProcessorExecutor(), 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /web/src/components/playground.styles.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import {css} from 'lit-element'; 21 | import {globalStyles} from '../styles'; 22 | 23 | const playgroundStyle = css` 24 | .playground { 25 | height: 100%; 26 | padding: 0 10px 0 10px; 27 | } 28 | 29 | .split-horizontal { 30 | display: flex; 31 | flex-direction: row; 32 | height: calc(100% - 90px); 33 | border: #eeeeee 1px solid; 34 | } 35 | 36 | .split-vertical { 37 | height: 100%; 38 | } 39 | 40 | .gutter { 41 | background-color: #eee; 42 | background-repeat: no-repeat; 43 | background-position: 50%; 44 | } 45 | 46 | .gutter.gutter-horizontal { 47 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); 48 | cursor: col-resize; 49 | } 50 | 51 | .gutter.gutter-vertical { 52 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII='); 53 | cursor: row-resize; 54 | } 55 | 56 | #loading { 57 | top: 0; 58 | padding-top: 25px; 59 | position: fixed; 60 | display: block; 61 | width: 100%; 62 | height: 100%; 63 | text-align: center; 64 | background-color: #fff; 65 | z-index: 99; 66 | } 67 | 68 | .beta-box { 69 | font-size: 10px !important; 70 | font-weight: 300 !important; 71 | color: gray; 72 | border: gray solid 1px; 73 | padding-left: 2px; 74 | padding-right: 2px; 75 | } 76 | `; 77 | 78 | export const playgroundStyles = [globalStyles, playgroundStyle]; 79 | -------------------------------------------------------------------------------- /web/src/styles.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import {css} from 'lit-element'; 21 | 22 | export const globalStyles = css` 23 | input[type='text'], 24 | select, 25 | textarea { 26 | width: 100%; 27 | padding: 8px; 28 | border: 1px solid #ccc; 29 | border-radius: 4px; 30 | resize: vertical; 31 | } 32 | 33 | button:disabled, 34 | button[disabled] { 35 | opacity: 0.6; 36 | cursor: not-allowed; 37 | } 38 | 39 | label { 40 | padding: 10px 10px 10px 0; 41 | display: inline-block; 42 | } 43 | 44 | .hidden-overflow { 45 | overflow: hidden; 46 | } 47 | 48 | .h-full { 49 | height: 100%; 50 | } 51 | 52 | .w-full { 53 | width: 100%; 54 | } 55 | 56 | .full-size { 57 | height: 100%; 58 | width: 100%; 59 | } 60 | 61 | .tooltip { 62 | position: relative; 63 | display: inline-block; 64 | } 65 | 66 | .tooltip .tooltip-text { 67 | visibility: hidden; 68 | width: 100%; 69 | background-color: rgb(84, 84, 84); 70 | color: #fff; 71 | text-align: center; 72 | padding: 5px 0; 73 | border-radius: 6px; 74 | position: absolute; 75 | z-index: 1; 76 | font-size: small; 77 | } 78 | 79 | .tooltip:hover .tooltip-text { 80 | visibility: visible; 81 | } 82 | 83 | .tooltip-text-position-right { 84 | top: -5px; 85 | left: 105%; 86 | } 87 | 88 | .tooltip-text-position-left { 89 | top: -5px; 90 | right: 105%; 91 | } 92 | 93 | .tooltip-text-position-bottom { 94 | width: 120px; 95 | top: 70%; 96 | left: 50%; 97 | margin-left: -60px; 98 | } 99 | `; 100 | -------------------------------------------------------------------------------- /testdata/profiles.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceProfiles": [ 3 | { 4 | "resource": { 5 | "attributes": [ 6 | { 7 | "key": "resource-attr", 8 | "value": { 9 | "stringValue": "resource-attr-val-1" 10 | } 11 | } 12 | ] 13 | }, 14 | "scopeProfiles": [ 15 | { 16 | "scope": {}, 17 | "profiles": [ 18 | { 19 | "sampleType": { 20 | "aggregationTemporality": 1 21 | }, 22 | "sample": [ 23 | { 24 | "stackIndex": 1, 25 | "values": [ 26 | "4" 27 | ], 28 | "attributeIndices": [ 29 | 0 30 | ] 31 | } 32 | ], 33 | "timeUnixNano": "1581452772000000321", 34 | "durationNano": "1581452773000000789", 35 | "periodType": { 36 | "aggregationTemporality": 1 37 | }, 38 | "profileId": "0102030405060708090a0b0c0d0e0f10", 39 | "droppedAttributesCount": 1 40 | }, 41 | { 42 | "sampleType": { 43 | "aggregationTemporality": 1 44 | }, 45 | "sample": [ 46 | { 47 | "stackIndex": 1, 48 | "values": [ 49 | "9" 50 | ], 51 | "attributeIndices": [ 52 | 0 53 | ] 54 | } 55 | ], 56 | "timeUnixNano": "1581452772000000321", 57 | "durationNano": "1581452773000000789", 58 | "periodType": { 59 | "aggregationTemporality": 1 60 | }, 61 | "profileId": "0202030405060708090a0b0c0d0e0f10" 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | ], 68 | "dictionary": { 69 | "locationTable": [ 70 | { 71 | "address": "1" 72 | }, 73 | { 74 | "address": "2" 75 | } 76 | ], 77 | "stringTable": [ 78 | "", 79 | "key" 80 | ], 81 | "attributeTable": [ 82 | { 83 | "keyStrindex": 1, 84 | "value": { 85 | "stringValue": "value" 86 | } 87 | }, 88 | { 89 | "value": { 90 | "stringValue": "value" 91 | } 92 | } 93 | ], 94 | "stackTable": [ 95 | { 96 | "locationIndices": [ 97 | 0 98 | ] 99 | }, 100 | { 101 | "locationIndices": [ 102 | 1 103 | ] 104 | } 105 | ] 106 | } 107 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOCMD?=go 2 | GO_BUILD_LDFLAGS?="-s -w" 3 | WEB_OUTPUT_DIR?=../web/public 4 | WASM_OUTPUT_DIR?=../web/public/wasm 5 | 6 | .PHONY: clean 7 | clean: 8 | rm -rf web/node_modules 9 | rm -rf web/public/wasm 10 | rm -f web/public/bundle.js 11 | rm -f web/public/bundle.js.map 12 | 13 | .PHONY: build-test-server 14 | build-build-test-server: 15 | CGO_ENABLED=0 $(GOCMD) build -ldflags $(GO_BUILD_LDFLAGS) -o server 16 | 17 | .PHONY: validate-registered-versions 18 | validate-registered-versions: 19 | $(GOCMD) run ci-tools/main.go validate-registered-versions 20 | 21 | .PHONY: build-unregistered-versions 22 | build-unregistered-versions: 23 | $(eval PROCESSORS_VERSIONS ?= $(shell $(GOCMD) run ci-tools/main.go get-unregistered-versions)) 24 | for v in $(PROCESSORS_VERSIONS); do \ 25 | export PROCESSORS_VERSION=$$v ; \ 26 | $(MAKE) update-processor-version && $(MAKE) build-wasm && $(MAKE) register-version ; \ 27 | done 28 | 29 | .PHONY: update-processor-version 30 | update-processor-version: 31 | $(eval PARAMS = $(shell $(GOCMD) run ci-tools/main.go generate-processors-update -version=$(PROCESSORS_VERSION))) 32 | $(GOCMD) get $(PARAMS) 33 | @FIRST_PROCESSOR=$$(echo "$(PARAMS)" | awk '{print $$1}'); \ 34 | COLLECTOR_DEPENDENCIES=$$($(GOCMD) mod graph | grep $$FIRST_PROCESSOR | grep "go.opentelemetry.io/collector/" | awk '{print $$2}' | sort -u); \ 35 | COLLECTOR_PARAMS=""; \ 36 | for DEP in $$COLLECTOR_DEPENDENCIES; do \ 37 | DEP_NAME=$$(echo $$DEP | cut -d'@' -f1); \ 38 | DEP_VERSION=$$(echo $$DEP | cut -d'@' -f2); \ 39 | if ! $(GOCMD) list -m -json $$DEP_NAME | grep -q "\"Indirect\":true"; then \ 40 | COLLECTOR_PARAMS="$$DEP_NAME@$$DEP_VERSION $$COLLECTOR_PARAMS"; \ 41 | fi; \ 42 | done; \ 43 | $(GOCMD) get $$COLLECTOR_PARAMS; 44 | $(MAKE) tidy 45 | 46 | .PHONY: build-wasm 47 | build-wasm: 48 | $(eval PROCESSORS_VERSION ?= $(shell $(GOCMD) run ci-tools/main.go get-version)) 49 | $(GOCMD) run ci-tools/main.go generate-constants -version=$(PROCESSORS_VERSION) 50 | cd wasm; GOARCH=wasm GOOS=js $(GOCMD) build -ldflags $(GO_BUILD_LDFLAGS) -o $(WASM_OUTPUT_DIR)/ottlplayground-$(PROCESSORS_VERSION).wasm 51 | 52 | .PHONY: register-version 53 | register-version: 54 | $(eval PROCESSORS_VERSION ?= $(shell go run ci-tools/main.go get-version)) 55 | $(GOCMD) run ci-tools/main.go register-wasm -version=$(PROCESSORS_VERSION) 56 | 57 | .PHONY: build-web 58 | build-web: 59 | cd web; npm install; npm run build 60 | 61 | .PHONY: update-wasm-exec 62 | update-wasm-exec: 63 | cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" web/src/wasm_exec.js || cp "$(shell go env GOROOT)/lib/wasm/wasm_exec.js" web/src/wasm_exec.js || true 64 | 65 | .PHONY: build 66 | build: update-wasm-exec build-web build-wasm register-version 67 | 68 | .PHONY: fmt 69 | fmt: 70 | gofmt -w -s ./ 71 | 72 | .PHONY: tidy 73 | tidy: 74 | rm -fr go.sum 75 | $(GOCMD) mod tidy -compat=1.22.0 -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build artifact 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | source_repository: 7 | description: 'Repository to checkout the playground code' 8 | required: true 9 | default: 'elastic/ottl-playground' 10 | source_branch: 11 | description: 'Branch to checkout the playground code' 12 | required: true 13 | default: 'main' 14 | wasm_compression: 15 | type: choice 16 | description: WASM files compression 17 | options: 18 | - none 19 | - brotli 20 | - gzip 21 | build_versions: 22 | type: number 23 | description: Additional opentelemetry-collector-contrib versions to compile and be supported by the playground. 24 | default: '10' 25 | 26 | workflow_call: 27 | inputs: 28 | source_repository: 29 | type: string 30 | required: true 31 | default: 'elastic/ottl-playground' 32 | source_branch: 33 | type: string 34 | required: true 35 | default: 'main' 36 | wasm_compression: 37 | type: string 38 | default: 'none' 39 | build_versions: 40 | type: number 41 | default: 10 42 | 43 | jobs: 44 | build: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Checkout playground code 48 | uses: actions/checkout@v4 49 | with: 50 | repository: ${{ inputs.source_repository }} 51 | ref: ${{ inputs.source_branch }} 52 | - name: Setup Node.js 53 | uses: actions/setup-node@v4 54 | with: 55 | node-version: '20.x' 56 | - name: Setup Go 57 | uses: actions/setup-go@v5 58 | with: 59 | go-version: '1.22.x' 60 | - name: Build 61 | run: make build 62 | - name: Build multiple versions 63 | if: inputs.build_versions > 0 64 | run: MAX_WASM_PROCESSORS_VERSIONS=${{ inputs.build_versions }} make build-unregistered-versions 65 | - name: Compress WASM files using brotli 66 | if: inputs.wasm_compression == 'brotli' 67 | run: | 68 | for file in web/public/wasm/ottlplayground-*.wasm ; do brotli -9j "$file" ; done 69 | for file in web/public/wasm/ottlplayground-*.wasm.br ; do mv "$file" "${file%.*}" ; done 70 | - name: Compress WASM files using gzip 71 | if: inputs.wasm_compression == 'gzip' 72 | run: | 73 | for file in web/public/wasm/ottlplayground-*.wasm ; do gzip -9 "$file" ; done 74 | for file in web/public/wasm/ottlplayground-*.wasm.gz ; do mv "$file" "${file%.*}" ; done 75 | - name: Validate registered versions 76 | run: make validate-registered-versions 77 | - name: Upload artifact 78 | uses: actions/upload-artifact@v4 79 | with: 80 | name: ottl-playground-package 81 | path: ./web/public 82 | retention-days: 1 83 | overwrite: true 84 | -------------------------------------------------------------------------------- /testdata/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceLogs": [ 3 | { 4 | "resource": { 5 | "attributes": [ 6 | { 7 | "key": "service.name", 8 | "value": { 9 | "stringValue": "my.service" 10 | } 11 | } 12 | ] 13 | }, 14 | "scopeLogs": [ 15 | { 16 | "scope": { 17 | "name": "my.library", 18 | "version": "1.0.0", 19 | "attributes": [ 20 | { 21 | "key": "my.scope.attribute", 22 | "value": { 23 | "stringValue": "some scope attribute" 24 | } 25 | } 26 | ] 27 | }, 28 | "logRecords": [ 29 | { 30 | "timeUnixNano": "1544712660300000000", 31 | "observedTimeUnixNano": "1544712660300000000", 32 | "severityNumber": 10, 33 | "severityText": "Information", 34 | "traceId": "5b8efff798038103d269b633813fc60c", 35 | "spanId": "eee19b7ec3c1b174", 36 | "body": { 37 | "stringValue": "Example log record" 38 | }, 39 | "attributes": [ 40 | { 41 | "key": "string.attribute", 42 | "value": { 43 | "stringValue": "some string" 44 | } 45 | }, 46 | { 47 | "key": "boolean.attribute", 48 | "value": { 49 | "boolValue": true 50 | } 51 | }, 52 | { 53 | "key": "int.attribute", 54 | "value": { 55 | "intValue": "10" 56 | } 57 | }, 58 | { 59 | "key": "double.attribute", 60 | "value": { 61 | "doubleValue": 637.704 62 | } 63 | }, 64 | { 65 | "key": "array.attribute", 66 | "value": { 67 | "arrayValue": { 68 | "values": [ 69 | { 70 | "stringValue": "many" 71 | }, 72 | { 73 | "stringValue": "values" 74 | } 75 | ] 76 | } 77 | } 78 | }, 79 | { 80 | "key": "map.attribute", 81 | "value": { 82 | "kvlistValue": { 83 | "values": [ 84 | { 85 | "key": "some.map.key", 86 | "value": { 87 | "stringValue": "some value" 88 | } 89 | } 90 | ] 91 | } 92 | } 93 | } 94 | ] 95 | } 96 | ] 97 | } 98 | ] 99 | } 100 | ] 101 | } -------------------------------------------------------------------------------- /internal/log_observer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package internal 21 | 22 | import ( 23 | "go.uber.org/zap/zapcore" 24 | "sync" 25 | ) 26 | 27 | type LoggedEntry struct { 28 | entry zapcore.Entry 29 | consoleEncodedEntry string 30 | } 31 | 32 | func (e *LoggedEntry) ConsoleEncodedEntry() string { 33 | return e.consoleEncodedEntry 34 | } 35 | 36 | type ObservedLogs struct { 37 | mu sync.RWMutex 38 | logs []LoggedEntry 39 | } 40 | 41 | func (o *ObservedLogs) Len() int { 42 | o.mu.RLock() 43 | n := len(o.logs) 44 | o.mu.RUnlock() 45 | return n 46 | } 47 | 48 | func (o *ObservedLogs) All() []LoggedEntry { 49 | o.mu.RLock() 50 | ret := make([]LoggedEntry, len(o.logs)) 51 | copy(ret, o.logs) 52 | o.mu.RUnlock() 53 | return ret 54 | } 55 | 56 | func (o *ObservedLogs) TakeAll() []LoggedEntry { 57 | o.mu.Lock() 58 | ret := o.logs 59 | o.logs = nil 60 | o.mu.Unlock() 61 | return ret 62 | } 63 | 64 | func (o *ObservedLogs) add(log LoggedEntry) { 65 | o.mu.Lock() 66 | o.logs = append(o.logs, log) 67 | o.mu.Unlock() 68 | } 69 | 70 | func NewLogObserver(level zapcore.LevelEnabler, config zapcore.EncoderConfig) (zapcore.Core, *ObservedLogs) { 71 | ol := &ObservedLogs{} 72 | return &contextObserver{ 73 | config: config, 74 | LevelEnabler: level, 75 | logs: ol, 76 | }, ol 77 | } 78 | 79 | type contextObserver struct { 80 | zapcore.LevelEnabler 81 | config zapcore.EncoderConfig 82 | logs *ObservedLogs 83 | context []zapcore.Field 84 | } 85 | 86 | var _ zapcore.Core = (*contextObserver)(nil) 87 | 88 | func (co *contextObserver) Level() zapcore.Level { 89 | return zapcore.LevelOf(co.LevelEnabler) 90 | } 91 | 92 | func (co *contextObserver) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { 93 | if co.Enabled(ent.Level) { 94 | return ce.AddCore(ent, co) 95 | } 96 | return ce 97 | } 98 | 99 | func (co *contextObserver) With(fields []zapcore.Field) zapcore.Core { 100 | return &contextObserver{ 101 | LevelEnabler: co.LevelEnabler, 102 | logs: co.logs, 103 | context: append(co.context[:len(co.context):len(co.context)], fields...), 104 | } 105 | } 106 | 107 | func (co *contextObserver) Write(entry zapcore.Entry, fields []zapcore.Field) error { 108 | encoder := zapcore.NewConsoleEncoder(co.config) 109 | encodedEntryBuffer, err := encoder.EncodeEntry(entry, fields) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | co.logs.add(LoggedEntry{entry, encodedEntryBuffer.String()}) 115 | return nil 116 | } 117 | 118 | func (co *contextObserver) Sync() error { 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /testdata/metrics.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceMetrics": [ 3 | { 4 | "resource": { 5 | "attributes": [ 6 | { 7 | "key": "service.name", 8 | "value": { 9 | "stringValue": "my.service" 10 | } 11 | } 12 | ] 13 | }, 14 | "scopeMetrics": [ 15 | { 16 | "scope": { 17 | "name": "my.library", 18 | "version": "1.0.0", 19 | "attributes": [ 20 | { 21 | "key": "my.scope.attribute", 22 | "value": { 23 | "stringValue": "some scope attribute" 24 | } 25 | } 26 | ] 27 | }, 28 | "metrics": [ 29 | { 30 | "name": "my.counter", 31 | "unit": "1", 32 | "description": "I am a Counter", 33 | "sum": { 34 | "aggregationTemporality": 1, 35 | "isMonotonic": true, 36 | "dataPoints": [ 37 | { 38 | "asDouble": 5, 39 | "startTimeUnixNano": "1544712660300000000", 40 | "timeUnixNano": "1544712660300000000", 41 | "attributes": [ 42 | { 43 | "key": "my.counter.attr", 44 | "value": { 45 | "stringValue": "some value" 46 | } 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | }, 53 | { 54 | "name": "my.gauge", 55 | "unit": "1", 56 | "description": "I am a Gauge", 57 | "gauge": { 58 | "dataPoints": [ 59 | { 60 | "asDouble": 10, 61 | "timeUnixNano": "1544712660300000000", 62 | "attributes": [ 63 | { 64 | "key": "my.gauge.attr", 65 | "value": { 66 | "stringValue": "some value" 67 | } 68 | } 69 | ] 70 | } 71 | ] 72 | } 73 | }, 74 | { 75 | "name": "my.histogram", 76 | "unit": "1", 77 | "description": "I am a Histogram", 78 | "histogram": { 79 | "aggregationTemporality": 1, 80 | "dataPoints": [ 81 | { 82 | "startTimeUnixNano": "1544712660300000000", 83 | "timeUnixNano": "1544712660300000000", 84 | "count": 2, 85 | "sum": 2, 86 | "bucketCounts": [ 87 | 1, 88 | 1 89 | ], 90 | "explicitBounds": [ 91 | 1 92 | ], 93 | "min": 0, 94 | "max": 2, 95 | "attributes": [ 96 | { 97 | "key": "my.histogram.attr", 98 | "value": { 99 | "stringValue": "some value" 100 | } 101 | } 102 | ] 103 | } 104 | ] 105 | } 106 | } 107 | ] 108 | } 109 | ] 110 | } 111 | ] 112 | } -------------------------------------------------------------------------------- /wasm/internal/ottlplayground.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package internal 21 | 22 | import ( 23 | "fmt" 24 | "strings" 25 | "time" 26 | 27 | "github.com/elastic/ottl-playground/internal" 28 | ) 29 | 30 | var ( 31 | statementsExecutors []internal.Executor 32 | statementsExecutorsLookup = map[string]internal.Executor{} 33 | ) 34 | 35 | func init() { 36 | for _, executor := range internal.Executors() { 37 | registerStatementsExecutor(executor) 38 | } 39 | } 40 | 41 | func registerStatementsExecutor(executor internal.Executor) { 42 | statementsExecutors = append(statementsExecutors, executor) 43 | statementsExecutorsLookup[executor.Metadata().ID] = executor 44 | } 45 | 46 | func newResult(json string, err string, logs string, executionTime int64) map[string]any { 47 | v := map[string]any{ 48 | "value": json, 49 | "logs": logs, 50 | "executionTime": executionTime, 51 | } 52 | if err != "" { 53 | v["error"] = err 54 | } 55 | return v 56 | } 57 | 58 | func NewErrorResult(err string, logs string) map[string]any { 59 | return newResult("", err, logs, 0) 60 | } 61 | 62 | func takeObservedLogs(executor internal.Executor) string { 63 | all := executor.ObservedLogs().TakeAll() 64 | var s strings.Builder 65 | for _, entry := range all { 66 | s.WriteString(entry.ConsoleEncodedEntry()) 67 | } 68 | return s.String() 69 | } 70 | 71 | func ExecuteStatements(config, ottlDataType, ottlDataPayload, executorName string) map[string]any { 72 | executor, ok := statementsExecutorsLookup[executorName] 73 | if !ok { 74 | return NewErrorResult(fmt.Sprintf("unsupported evaluator %s", executorName), "") 75 | } 76 | 77 | start := time.Now() 78 | var output []byte 79 | var err error 80 | switch ottlDataType { 81 | case "logs": 82 | output, err = executor.ExecuteLogStatements(config, ottlDataPayload) 83 | case "traces": 84 | output, err = executor.ExecuteTraceStatements(config, ottlDataPayload) 85 | case "metrics": 86 | output, err = executor.ExecuteMetricStatements(config, ottlDataPayload) 87 | case "profiles": 88 | output, err = executor.ExecuteProfileStatements(config, ottlDataPayload) 89 | default: 90 | return NewErrorResult(fmt.Sprintf("unsupported OTLP data type %s", ottlDataType), "") 91 | } 92 | 93 | if err != nil { 94 | return NewErrorResult(fmt.Sprintf("unable to run %s statements. Error: %v", ottlDataType, err), takeObservedLogs(executor)) 95 | } 96 | 97 | executionTime := time.Since(start).Milliseconds() 98 | return newResult(string(output), "", takeObservedLogs(executor), executionTime) 99 | } 100 | 101 | func StatementsExecutors() []any { 102 | var res []any 103 | for _, executor := range statementsExecutors { 104 | meta := executor.Metadata() 105 | res = append(res, map[string]any{ 106 | "id": meta.ID, 107 | "name": meta.Name, 108 | "path": meta.Path, 109 | "docsURL": meta.DocsURL, 110 | "version": meta.Version, 111 | }) 112 | } 113 | return res 114 | } 115 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/elastic/ottl-playground 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor v0.138.0 7 | github.com/open-telemetry/opentelemetry-collector-contrib/processor/transformprocessor v0.138.0 8 | github.com/stretchr/testify v1.11.1 9 | go.opentelemetry.io/collector/component v1.44.0 10 | go.opentelemetry.io/collector/component/componenttest v0.138.0 11 | go.opentelemetry.io/collector/confmap v1.44.0 12 | go.opentelemetry.io/collector/consumer v1.44.0 13 | go.opentelemetry.io/collector/consumer/xconsumer v0.138.0 14 | go.opentelemetry.io/collector/pdata v1.44.0 15 | go.opentelemetry.io/collector/pdata/pprofile v0.138.0 16 | go.opentelemetry.io/collector/processor v1.44.0 17 | go.opentelemetry.io/collector/processor/xprocessor v0.138.0 18 | go.uber.org/zap v1.27.0 19 | golang.org/x/mod v0.29.0 20 | ) 21 | 22 | require ( 23 | github.com/alecthomas/participle/v2 v2.1.4 // indirect 24 | github.com/antchfx/xmlquery v1.5.0 // indirect 25 | github.com/antchfx/xpath v1.3.5 // indirect 26 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 27 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 28 | github.com/elastic/go-grok v0.3.1 // indirect 29 | github.com/elastic/lunes v0.1.0 // indirect 30 | github.com/expr-lang/expr v1.17.6 // indirect 31 | github.com/go-logr/logr v1.4.3 // indirect 32 | github.com/go-logr/stdr v1.2.2 // indirect 33 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 34 | github.com/gobwas/glob v0.2.3 // indirect 35 | github.com/goccy/go-json v0.10.5 // indirect 36 | github.com/gogo/protobuf v1.3.2 // indirect 37 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 38 | github.com/google/uuid v1.6.0 // indirect 39 | github.com/hashicorp/go-version v1.7.0 // indirect 40 | github.com/hashicorp/golang-lru v1.0.2 // indirect 41 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 42 | github.com/iancoleman/strcase v0.3.0 // indirect 43 | github.com/json-iterator/go v1.1.12 // indirect 44 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect 45 | github.com/knadh/koanf/maps v0.1.2 // indirect 46 | github.com/knadh/koanf/providers/confmap v1.0.0 // indirect 47 | github.com/knadh/koanf/v2 v2.3.0 // indirect 48 | github.com/lightstep/go-expohisto v1.0.0 // indirect 49 | github.com/magefile/mage v1.15.0 // indirect 50 | github.com/mitchellh/copystructure v1.2.0 // indirect 51 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 53 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 54 | github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.138.0 // indirect 55 | github.com/open-telemetry/opentelemetry-collector-contrib/internal/filter v0.138.0 // indirect 56 | github.com/open-telemetry/opentelemetry-collector-contrib/internal/pdatautil v0.138.0 // indirect 57 | github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl v0.138.0 // indirect 58 | github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.138.0 // indirect 59 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 60 | github.com/stretchr/objx v0.5.2 // indirect 61 | github.com/twmb/murmur3 v1.1.8 // indirect 62 | github.com/ua-parser/uap-go v0.0.0-20250326155420-f7f5a2f9f5bc // indirect 63 | github.com/zeebo/xxh3 v1.0.2 // indirect 64 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 65 | go.opentelemetry.io/collector/featuregate v1.44.0 // indirect 66 | go.opentelemetry.io/collector/internal/telemetry v0.138.0 // indirect 67 | go.opentelemetry.io/collector/pipeline v1.44.0 // indirect 68 | go.opentelemetry.io/collector/pipeline/xpipeline v0.138.0 // indirect 69 | go.opentelemetry.io/collector/processor/processorhelper v0.138.0 // indirect 70 | go.opentelemetry.io/collector/processor/processorhelper/xprocessorhelper v0.138.0 // indirect 71 | go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 // indirect 72 | go.opentelemetry.io/otel v1.38.0 // indirect 73 | go.opentelemetry.io/otel/log v0.14.0 // indirect 74 | go.opentelemetry.io/otel/metric v1.38.0 // indirect 75 | go.opentelemetry.io/otel/sdk v1.38.0 // indirect 76 | go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect 77 | go.opentelemetry.io/otel/trace v1.38.0 // indirect 78 | go.uber.org/multierr v1.11.0 // indirect 79 | go.yaml.in/yaml/v3 v3.0.4 // indirect 80 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect 81 | golang.org/x/net v0.44.0 // indirect 82 | golang.org/x/sys v0.36.0 // indirect 83 | golang.org/x/text v0.29.0 // indirect 84 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect 85 | google.golang.org/grpc v1.76.0 // indirect 86 | google.golang.org/protobuf v1.36.10 // indirect 87 | gopkg.in/yaml.v3 v3.0.1 // indirect 88 | ) 89 | -------------------------------------------------------------------------------- /internal/processorexecutor_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package internal 21 | 22 | import ( 23 | "os" 24 | "path/filepath" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/stretchr/testify/require" 29 | "go.opentelemetry.io/collector/pdata/plog" 30 | "go.opentelemetry.io/collector/pdata/pmetric" 31 | "go.opentelemetry.io/collector/pdata/pprofile" 32 | "go.opentelemetry.io/collector/pdata/ptrace" 33 | 34 | "github.com/open-telemetry/opentelemetry-collector-contrib/processor/transformprocessor" 35 | ) 36 | 37 | func Test_ParseConfig_Success(t *testing.T) { 38 | executor := newProcessorExecutor[transformprocessor.Config](transformprocessor.NewFactory()) 39 | config := readTestData(t, transformprocessorConfig) 40 | parsedConfig, err := executor.parseConfig(config) 41 | 42 | require.NoError(t, err) 43 | require.NotNil(t, parsedConfig) 44 | require.NotEmpty(t, parsedConfig.ErrorMode) 45 | require.NotEmpty(t, parsedConfig.TraceStatements) 46 | require.NotEmpty(t, parsedConfig.MetricStatements) 47 | require.NotEmpty(t, parsedConfig.MetricStatements) 48 | } 49 | 50 | func Test_ParseConfig_Error(t *testing.T) { 51 | executor := newProcessorExecutor[transformprocessor.Config](transformprocessor.NewFactory()) 52 | _, err := executor.parseConfig("---invalid---") 53 | require.ErrorContains(t, err, "cannot be used as a Conf") 54 | } 55 | 56 | func Test_ExecuteLogStatements(t *testing.T) { 57 | executor := newProcessorExecutor[transformprocessor.Config](transformprocessor.NewFactory()) 58 | config := readTestData(t, transformprocessorConfig) 59 | payload := readTestData(t, "logs.json") 60 | 61 | output, err := executor.ExecuteLogStatements(config, payload) 62 | require.NoError(t, err) 63 | 64 | unmarshaler := &plog.JSONUnmarshaler{} 65 | outputLogs, err := unmarshaler.UnmarshalLogs(output) 66 | require.NoError(t, err) 67 | require.NotNil(t, outputLogs) 68 | } 69 | 70 | func Test_ExecuteTraceStatements(t *testing.T) { 71 | executor := newProcessorExecutor[transformprocessor.Config](transformprocessor.NewFactory()) 72 | config := readTestData(t, transformprocessorConfig) 73 | payload := readTestData(t, "traces.json") 74 | 75 | output, err := executor.ExecuteTraceStatements(config, payload) 76 | require.NoError(t, err) 77 | 78 | unmarshaler := &ptrace.JSONUnmarshaler{} 79 | outputTraces, err := unmarshaler.UnmarshalTraces(output) 80 | require.NoError(t, err) 81 | require.NotNil(t, outputTraces) 82 | } 83 | 84 | func Test_ExecuteMetricStatements(t *testing.T) { 85 | executor := newProcessorExecutor[transformprocessor.Config](transformprocessor.NewFactory()) 86 | config := readTestData(t, transformprocessorConfig) 87 | payload := readTestData(t, "metrics.json") 88 | 89 | output, err := executor.ExecuteMetricStatements(config, payload) 90 | require.NoError(t, err) 91 | 92 | unmarshaler := &pmetric.JSONUnmarshaler{} 93 | outputMetrics, err := unmarshaler.UnmarshalMetrics(output) 94 | require.NoError(t, err) 95 | require.NotNil(t, outputMetrics) 96 | } 97 | 98 | func Test_ExecuteProfileStatements(t *testing.T) { 99 | executor := newProcessorExecutor[transformprocessor.Config](transformprocessor.NewFactory()) 100 | config := readTestData(t, transformprocessorConfig) 101 | payload := readTestData(t, "profiles.json") 102 | 103 | output, err := executor.ExecuteProfileStatements(config, payload) 104 | require.NoError(t, err) 105 | 106 | unmarshaler := &pprofile.JSONUnmarshaler{} 107 | outputProfiles, err := unmarshaler.UnmarshalProfiles(output) 108 | require.NoError(t, err) 109 | require.NotNil(t, outputProfiles) 110 | } 111 | 112 | func Test_ObservedLogs(t *testing.T) { 113 | executor := newProcessorExecutor[transformprocessor.Config](transformprocessor.NewFactory()) 114 | executor.settings.Logger.Sugar().Debug("this is a log") 115 | logEntries := executor.ObservedLogs().TakeAll() 116 | assert.Len(t, logEntries, 1) 117 | assert.Contains(t, logEntries[0].ConsoleEncodedEntry(), "this is a log") 118 | } 119 | 120 | func readTestData(t *testing.T, file string) string { 121 | content, err := os.ReadFile(filepath.Join("..", "testdata", file)) 122 | require.NoError(t, err) 123 | return string(content) 124 | } 125 | -------------------------------------------------------------------------------- /web/src/components/controls/copy-link-button.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import {css, html, LitElement} from 'lit-element'; 21 | import {globalStyles} from '../../styles'; 22 | 23 | export class PlaygroundCopyLinkButton extends LitElement { 24 | static properties = { 25 | label: {}, 26 | loading: {type: Boolean}, 27 | buttonTip: {state: true}, 28 | }; 29 | 30 | constructor() { 31 | super(); 32 | this.label = 'Copy link'; 33 | this.loading = false; 34 | } 35 | 36 | static get styles() { 37 | return [ 38 | globalStyles, 39 | css` 40 | .link-button { 41 | background-color: #e8e7e7; 42 | border: 1px solid #dcdbdb; 43 | color: black; 44 | padding: 10px 4px 10px 4px; 45 | width: 110px; 46 | text-align: center; 47 | text-decoration: none; 48 | font-size: 16px; 49 | cursor: pointer; 50 | border-radius: 4px; 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | } 55 | 56 | .link-button-label { 57 | color: #4b4949; 58 | } 59 | 60 | .link-button:hover { 61 | background-color: #dedede; 62 | } 63 | 64 | .tooltip:hover .tooltip-text { 65 | visibility: hidden; 66 | } 67 | `, 68 | ]; 69 | } 70 | 71 | render() { 72 | return html` 73 | 97 | `; 98 | } 99 | 100 | async _handleCopyLinkClick() { 101 | this.loading = true; 102 | setTimeout(() => { 103 | let copied = this.dispatchEvent( 104 | new CustomEvent('copy-link-click', { 105 | composed: true, 106 | cancelable: true, 107 | }) 108 | ); 109 | 110 | if (copied) { 111 | this.shadowRoot.querySelector('#copied-tooltip').style.visibility = 112 | 'visible'; 113 | setTimeout(() => { 114 | this.shadowRoot.querySelector('#copied-tooltip').style.visibility = 115 | 'hidden'; 116 | }, 1500); 117 | } 118 | this.loading = false; 119 | }, 0); 120 | } 121 | } 122 | 123 | customElements.define('playground-copy-link-button', PlaygroundCopyLinkButton); 124 | -------------------------------------------------------------------------------- /web/src/components/panels/config-panel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import {html, LitElement} from 'lit-element'; 21 | import {codePanelsStyles} from './styles'; 22 | import {basicSetup, EditorView} from 'codemirror'; 23 | import {Prec} from '@codemirror/state'; 24 | import {keymap} from '@codemirror/view'; 25 | import {indentWithTab, insertNewlineAndIndent} from '@codemirror/commands'; 26 | import {yaml} from '@codemirror/lang-yaml'; 27 | import {nothing} from 'lit'; 28 | import {repeat} from 'lit/directives/repeat.js'; 29 | 30 | export class PlaygroundConfigPanel extends LitElement { 31 | static properties = { 32 | examples: {type: Array}, 33 | hideExamples: {type: Boolean, attribute: 'hide-examples'}, 34 | config: {type: String}, 35 | configDocsURL: {type: String, attribute: 'config-docs-url'}, 36 | _editor: {state: true}, 37 | }; 38 | 39 | constructor() { 40 | super(); 41 | this.hideExamples = false; 42 | this.examples = []; 43 | this.configDocsURL = ''; 44 | } 45 | 46 | static get styles() { 47 | return codePanelsStyles; 48 | } 49 | 50 | get config() { 51 | return this._editor?.state.doc.toString() ?? ''; 52 | } 53 | 54 | set config(value) { 55 | if (value === this.config) return; 56 | this.updateComplete.then(() => { 57 | this._editor?.dispatch({ 58 | changes: {from: 0, to: this._editor.state.doc.length, insert: value}, 59 | }); 60 | }); 61 | } 62 | 63 | firstUpdated() { 64 | this._initCodeEditor(); 65 | } 66 | 67 | updated(changedProperties) { 68 | if (changedProperties.has('examples')) { 69 | // Reset the selected example 70 | this.shadowRoot.querySelector('#example-input').value = ''; 71 | } 72 | super.updated(changedProperties); 73 | } 74 | 75 | render() { 76 | return html` 77 |
78 |
79 |
80 | 81 | Configuration 82 | 83 | 84 | YAML 85 | 86 | 87 | 88 |
89 |
90 | ${this.hideExamples 91 | ? nothing 92 | : html` 93 | 111 | `} 112 | 113 |
114 |
115 |
116 |
117 |
118 |
119 | `; 120 | } 121 | 122 | _handleExampleChanged(event) { 123 | if (!event.target.value) return; 124 | let idx = parseInt(event.target.value); 125 | let example = this.examples[idx]; 126 | if (!example) return; 127 | 128 | this.config = example.config; 129 | this.dispatchEvent( 130 | new CustomEvent('config-example-changed', { 131 | detail: {value: example}, 132 | bubbles: true, 133 | composed: true, 134 | cancelable: true, 135 | }) 136 | ); 137 | } 138 | 139 | _notifyConfigChange(value) { 140 | this.dispatchEvent( 141 | new CustomEvent('config-changed', { 142 | detail: {value: value}, 143 | bubbles: true, 144 | composed: true, 145 | }) 146 | ); 147 | } 148 | 149 | _initCodeEditor() { 150 | this._editor = new EditorView({ 151 | extensions: [ 152 | basicSetup, 153 | Prec.highest( 154 | keymap.of([ 155 | indentWithTab, 156 | {key: 'Enter', run: insertNewlineAndIndent, shift: () => true}, 157 | ]) 158 | ), 159 | EditorView.lineWrapping, 160 | yaml(), 161 | EditorView.updateListener.of((v) => { 162 | if (v.docChanged) { 163 | this._notifyConfigChange(this.config); 164 | } 165 | }), 166 | ], 167 | parent: this.shadowRoot.querySelector('#config-input'), 168 | }); 169 | } 170 | } 171 | 172 | customElements.define('playground-config-panel', PlaygroundConfigPanel); 173 | -------------------------------------------------------------------------------- /web/src/components/panels/payload-panel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import {css, html, LitElement} from 'lit-element'; 21 | import {codePanelsStyles} from './styles'; 22 | import {basicSetup, EditorView} from 'codemirror'; 23 | import {keymap} from '@codemirror/view'; 24 | import {indentWithTab, insertNewlineAndIndent} from '@codemirror/commands'; 25 | import {PAYLOAD_EXAMPLES} from '../examples'; 26 | import {linter, lintGutter} from '@codemirror/lint'; 27 | import {json, jsonParseLinter} from '@codemirror/lang-json'; 28 | import {nothing} from 'lit'; 29 | import {Prec} from '@codemirror/state'; 30 | 31 | export class PlaygroundPayloadPanel extends LitElement { 32 | static properties = { 33 | payload: {type: String}, 34 | hideExamples: {type: Boolean, attribute: 'hide-examples'}, 35 | _editor: {type: Object, state: true}, 36 | }; 37 | 38 | constructor() { 39 | super(); 40 | this.payload = '{}'; 41 | this.hideLoadExample = false; 42 | } 43 | 44 | static get styles() { 45 | return [ 46 | css` 47 | .example-button { 48 | background-color: #f1f1f1; 49 | border: none; 50 | color: black; 51 | font-size: 28px; 52 | padding: 0 12px; 53 | text-align: center; 54 | text-decoration: none; 55 | display: inline-block; 56 | margin: 4px 2px; 57 | cursor: pointer; 58 | border-radius: 5px; 59 | } 60 | `, 61 | ...codePanelsStyles, 62 | ]; 63 | } 64 | 65 | get payload() { 66 | return this._editor?.state.doc.toString() ?? ''; 67 | } 68 | 69 | set payload(value) { 70 | if (value === this.payload) return; 71 | this.updateComplete.then(() => { 72 | this._editor?.dispatch({ 73 | changes: {from: 0, to: this._editor.state.doc.length, insert: value}, 74 | }); 75 | }); 76 | } 77 | 78 | set selectedExample(val) { 79 | this.shadowRoot.querySelector('#example-select').value = val; 80 | } 81 | 82 | firstUpdated() { 83 | this._initCodeEditor(); 84 | } 85 | 86 | render() { 87 | return html` 88 |
89 |
90 |
91 | OTLP payload 93 | JSON 103 |
104 |
105 | ${this.hideLoadExample 106 | ? nothing 107 | : html` 108 | 118 |
`} 119 |
120 |
121 |
122 |
123 |
124 | 125 | `; 126 | } 127 | 128 | _handleExampleChanged(e) { 129 | if (!e.target.value) return; 130 | let val = JSON.stringify( 131 | JSON.parse(PAYLOAD_EXAMPLES[e.target.value]), 132 | null, 133 | 2 134 | ); 135 | this._editor?.dispatch({ 136 | changes: {from: 0, to: this._editor.state.doc.length, insert: val}, 137 | }); 138 | } 139 | 140 | _notifyPayloadChange(value) { 141 | this.dispatchEvent( 142 | new CustomEvent('payload-changed', { 143 | detail: {value: value}, 144 | bubbles: true, 145 | composed: true, 146 | }) 147 | ); 148 | } 149 | 150 | _initCodeEditor() { 151 | this._editor = new EditorView({ 152 | extensions: [ 153 | basicSetup, 154 | Prec.highest( 155 | keymap.of([ 156 | indentWithTab, 157 | {key: 'Enter', run: insertNewlineAndIndent, shift: () => true}, 158 | ]) 159 | ), 160 | linter(jsonParseLinter()), 161 | lintGutter(), 162 | EditorView.lineWrapping, 163 | EditorView.updateListener.of((v) => { 164 | if (v.docChanged) { 165 | this._notifyPayloadChange(this.payload); 166 | } 167 | }), 168 | json(), 169 | ], 170 | parent: this.shadowRoot.querySelector('#otlp-data-input'), 171 | }); 172 | } 173 | } 174 | 175 | customElements.define('playground-payload-panel', PlaygroundPayloadPanel); 176 | -------------------------------------------------------------------------------- /web/src/components/user-consent-banner/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import {LitElement, html, css, nothing} from 'lit'; 21 | 22 | class PlaygroundUserConsentBanner extends LitElement { 23 | static properties = { 24 | _hasUserConsent: {state: true}, 25 | }; 26 | 27 | constructor() { 28 | super(); 29 | let isEmbedded = window.self !== window.top; 30 | this._hasUserConsent = isEmbedded || this._getCookie('userConsent'); 31 | } 32 | 33 | static styles = css` 34 | .user-consent-banner { 35 | position: fixed; 36 | bottom: 0; 37 | left: 0; 38 | z-index: 2147483645; 39 | box-sizing: border-box; 40 | width: 100%; 41 | background-color: rgb(245, 245, 245); 42 | max-height: 600px; 43 | overflow-y: auto; 44 | border-top-style: outset; 45 | border-top-width: 1px; 46 | } 47 | 48 | .user-consent-banner-inner { 49 | width: 65%; 50 | margin: 0 auto; 51 | padding: 10px; 52 | } 53 | 54 | .user-consent-banner-copy { 55 | margin-bottom: 16px; 56 | } 57 | 58 | .user-consent-banner-header { 59 | margin-bottom: 8px; 60 | font-weight: 500; 61 | font-size: 14px; 62 | line-height: 15px; 63 | } 64 | 65 | .user-consent-banner-description { 66 | font-weight: normal; 67 | color: #838f93; 68 | font-size: 12px; 69 | line-height: 13px; 70 | text-align: justify; 71 | width: 100%; 72 | } 73 | 74 | .user-consent-banner-button { 75 | box-sizing: border-box; 76 | display: inline-block; 77 | min-width: 130px; 78 | padding: 7px; 79 | border-radius: 4px; 80 | background-color: #0073ce; 81 | border: none; 82 | cursor: pointer; 83 | color: #fff; 84 | text-decoration: none; 85 | text-align: center; 86 | font-weight: normal; 87 | font-size: 13px; 88 | line-height: 13px; 89 | } 90 | 91 | .user-consent-banner-button--secondary { 92 | padding: 9px 13px; 93 | border: 2px solid #3a4649; 94 | background-color: transparent; 95 | color: #0073ce; 96 | } 97 | 98 | .user-consent-banner-button:hover { 99 | box-shadow: 0 0 0 999px inset rgba(0, 0, 0, 0.1) !important; 100 | } 101 | 102 | .user-consent-banner-button--secondary:hover { 103 | border-color: #838f93; 104 | background-color: transparent; 105 | box-shadow: 0 0 0 999px inset rgba(0, 0, 0, 0.1) !important; 106 | } 107 | `; 108 | 109 | render() { 110 | return this._hasUserConsent 111 | ? nothing 112 | : html` 113 | 159 | `; 160 | } 161 | 162 | acknowledgeNotice() { 163 | this._setCookie('userConsent', 'true', 365); 164 | this._hasUserConsent = true; 165 | } 166 | 167 | _setCookie(name, value, days) { 168 | let expires = ''; 169 | if (days) { 170 | const date = new Date(); 171 | date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); 172 | expires = '; expires=' + date.toUTCString(); 173 | } 174 | document.cookie = name + '=' + (value || '') + expires + '; path=/'; 175 | } 176 | 177 | _getCookie(name) { 178 | const nameEQ = name + '='; 179 | const ca = document.cookie.split(';'); 180 | for (let i = 0; i < ca.length; i++) { 181 | let c = ca[i]; 182 | while (c.charAt(0) === ' ') c = c.substring(1, c.length); 183 | if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); 184 | } 185 | return null; 186 | } 187 | } 188 | 189 | customElements.define( 190 | 'playground-user-consent-banner', 191 | PlaygroundUserConsentBanner 192 | ); 193 | -------------------------------------------------------------------------------- /wasm/internal/ottlplayground_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package internal 21 | 22 | import ( 23 | "errors" 24 | "fmt" 25 | "testing" 26 | 27 | "github.com/elastic/ottl-playground/internal" 28 | "github.com/stretchr/testify/assert" 29 | "github.com/stretchr/testify/mock" 30 | "go.uber.org/zap" 31 | ) 32 | 33 | func Test_NewErrorResult(t *testing.T) { 34 | logs := "execution logs" 35 | err := "error" 36 | expected := map[string]any{ 37 | "value": "", 38 | "logs": logs, 39 | "error": err, 40 | "executionTime": int64(0), 41 | } 42 | 43 | result := NewErrorResult(err, logs) 44 | assert.Equal(t, expected, result) 45 | } 46 | 47 | func Test_ExecuteStatements_UnsupportedExecutor(t *testing.T) { 48 | config := "empty" 49 | otlpDataType := "logs" 50 | otlpDataPayload := "{}" 51 | executorName := "unsupported_processor" 52 | 53 | expectedError := fmt.Sprintf("unsupported evaluator %s", executorName) 54 | result := ExecuteStatements(config, otlpDataType, otlpDataPayload, executorName) 55 | assert.Equal(t, "", result["value"]) 56 | assert.Equal(t, "", result["logs"]) 57 | assert.Equal(t, expectedError, result["error"]) 58 | assert.GreaterOrEqual(t, result["executionTime"], int64(0)) 59 | } 60 | 61 | func Test_ExecuteStatements_UnsupportedOTLPType(t *testing.T) { 62 | config := "empty" 63 | otlpDataType := "unsupported_datatype" 64 | otlpDataPayload := "{}" 65 | executorName := "transform_processor" 66 | 67 | expectedError := fmt.Sprintf("unsupported OTLP data type %s", otlpDataType) 68 | 69 | result := ExecuteStatements(config, otlpDataType, otlpDataPayload, executorName) 70 | assert.Equal(t, "", result["value"]) 71 | assert.Equal(t, "", result["logs"]) 72 | assert.Equal(t, expectedError, result["error"]) 73 | assert.GreaterOrEqual(t, result["executionTime"], int64(0)) 74 | } 75 | 76 | func Test_ExecuteStatements(t *testing.T) { 77 | tests := []struct { 78 | name string 79 | otlpDataType string 80 | executorFunc string 81 | expectedOutput string 82 | expectedError error 83 | }{ 84 | { 85 | name: "Logs Success", 86 | otlpDataType: "logs", 87 | executorFunc: "ExecuteLogStatements", 88 | expectedOutput: "log output", 89 | }, 90 | { 91 | name: "Logs Error", 92 | otlpDataType: "logs", 93 | executorFunc: "ExecuteLogStatements", 94 | expectedError: errors.New("ExecuteLogStatements execution error"), 95 | }, 96 | { 97 | name: "Traces Success", 98 | otlpDataType: "traces", 99 | executorFunc: "ExecuteTraceStatements", 100 | expectedOutput: "trace output", 101 | }, 102 | { 103 | name: "Traces Error", 104 | otlpDataType: "traces", 105 | executorFunc: "ExecuteTraceStatements", 106 | expectedError: errors.New("ExecuteTraceStatements execution error"), 107 | }, 108 | { 109 | name: "Metrics Success", 110 | otlpDataType: "metrics", 111 | executorFunc: "ExecuteMetricStatements", 112 | expectedOutput: "metric output", 113 | }, 114 | { 115 | name: "Metrics Error", 116 | otlpDataType: "metrics", 117 | executorFunc: "ExecuteMetricStatements", 118 | expectedError: errors.New("ExecuteMetricStatements execution error"), 119 | }, 120 | { 121 | name: "Profiles Success", 122 | otlpDataType: "profiles", 123 | executorFunc: "ExecuteProfileStatements", 124 | expectedOutput: "profile output", 125 | }, 126 | { 127 | name: "Profiles Error", 128 | otlpDataType: "profiles", 129 | executorFunc: "ExecuteProfileStatements", 130 | expectedError: errors.New("ExecuteProfileStatements execution error"), 131 | }, 132 | } 133 | 134 | var ( 135 | testConfig = "empty" 136 | ottlDataPayload = "{}" 137 | ) 138 | 139 | _, observedLogs := internal.NewLogObserver(zap.NewNop().Core(), zap.NewDevelopmentEncoderConfig()) 140 | for _, tt := range tests { 141 | t.Run(tt.name, func(t *testing.T) { 142 | executorName := tt.name 143 | mockExecutor := &MockExecutor{ 144 | metadata: internal.Metadata{ 145 | ID: executorName, 146 | Name: executorName, 147 | }, 148 | } 149 | 150 | registerStatementsExecutor(mockExecutor) 151 | mockExecutor.On(tt.executorFunc, testConfig, ottlDataPayload).Return(tt.expectedOutput, tt.expectedError) 152 | mockExecutor.On("ObservedLogs").Return(observedLogs) 153 | 154 | result := ExecuteStatements(testConfig, tt.otlpDataType, ottlDataPayload, executorName) 155 | 156 | if tt.expectedError != nil { 157 | assert.Empty(t, result["value"]) 158 | expectedErrorMsg := fmt.Sprintf("unable to run %s statements. Error: %v", tt.otlpDataType, tt.expectedError) 159 | assert.Contains(t, result["error"], expectedErrorMsg) 160 | assert.Equal(t, result["executionTime"], int64(0)) 161 | } else { 162 | assert.Equal(t, tt.expectedOutput, result["value"]) 163 | assert.NotContains(t, result, "error") 164 | assert.GreaterOrEqual(t, result["executionTime"], int64(0)) 165 | } 166 | 167 | mockExecutor.AssertExpectations(t) 168 | }) 169 | } 170 | } 171 | 172 | func Test_TakeObserved_Logs(t *testing.T) { 173 | mockExecutor := new(MockExecutor) 174 | core, observedLogs := internal.NewLogObserver(zap.DebugLevel, zap.NewDevelopmentEncoderConfig()) 175 | mockExecutor.On("ObservedLogs").Return(observedLogs) 176 | 177 | logger := zap.New(core) 178 | logger.Debug("debug logs") 179 | 180 | logs := takeObservedLogs(mockExecutor) 181 | 182 | assert.Contains(t, logs, "debug logs") 183 | mockExecutor.AssertExpectations(t) 184 | } 185 | 186 | type MockExecutor struct { 187 | mock.Mock 188 | metadata internal.Metadata 189 | } 190 | 191 | func (m *MockExecutor) ExecuteLogStatements(config, payload string) ([]byte, error) { 192 | args := m.Called(config, payload) 193 | return []byte(args.String(0)), args.Error(1) 194 | } 195 | 196 | func (m *MockExecutor) ExecuteTraceStatements(config, payload string) ([]byte, error) { 197 | args := m.Called(config, payload) 198 | return []byte(args.String(0)), args.Error(1) 199 | } 200 | 201 | func (m *MockExecutor) ExecuteMetricStatements(config, payload string) ([]byte, error) { 202 | args := m.Called(config, payload) 203 | return []byte(args.String(0)), args.Error(1) 204 | } 205 | 206 | func (m *MockExecutor) ExecuteProfileStatements(config, payload string) ([]byte, error) { 207 | args := m.Called(config, payload) 208 | return []byte(args.String(0)), args.Error(1) 209 | } 210 | 211 | func (m *MockExecutor) ObservedLogs() *internal.ObservedLogs { 212 | args := m.Called() 213 | return args.Get(0).(*internal.ObservedLogs) 214 | } 215 | 216 | func (m *MockExecutor) Metadata() internal.Metadata { 217 | return m.metadata 218 | } 219 | -------------------------------------------------------------------------------- /internal/processorexecutor.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package internal 21 | 22 | import ( 23 | "context" 24 | "errors" 25 | "strings" 26 | 27 | "go.opentelemetry.io/collector/consumer/xconsumer" 28 | "go.opentelemetry.io/collector/pdata/pprofile" 29 | "go.opentelemetry.io/collector/processor/xprocessor" 30 | 31 | "go.opentelemetry.io/collector/component" 32 | "go.opentelemetry.io/collector/component/componenttest" 33 | "go.opentelemetry.io/collector/confmap" 34 | "go.opentelemetry.io/collector/consumer" 35 | "go.opentelemetry.io/collector/pdata/plog" 36 | "go.opentelemetry.io/collector/pdata/pmetric" 37 | "go.opentelemetry.io/collector/pdata/ptrace" 38 | "go.opentelemetry.io/collector/processor" 39 | "go.uber.org/zap" 40 | "go.uber.org/zap/zapcore" 41 | ) 42 | 43 | type processorExecutor[T any] struct { 44 | factory processor.Factory 45 | settings processor.Settings 46 | telemetrySettings component.TelemetrySettings 47 | observedLogs *ObservedLogs 48 | } 49 | 50 | func newProcessorExecutor[C any](factory processor.Factory) *processorExecutor[C] { 51 | observedLogger, observedLogs := NewLogObserver(zap.DebugLevel, zap.NewDevelopmentEncoderConfig()) 52 | logger, _ := zap.NewDevelopmentConfig().Build(zap.WrapCore(func(z zapcore.Core) zapcore.Core { 53 | return observedLogger 54 | })) 55 | 56 | telemetrySettings := componenttest.NewNopTelemetrySettings() 57 | telemetrySettings.Logger = logger 58 | settings := processor.Settings{ 59 | ID: component.MustNewIDWithName(factory.Type().String(), "ottl_playground"), 60 | TelemetrySettings: telemetrySettings, 61 | } 62 | 63 | return &processorExecutor[C]{ 64 | factory: factory, 65 | telemetrySettings: telemetrySettings, 66 | settings: settings, 67 | observedLogs: observedLogs, 68 | } 69 | } 70 | 71 | func (p *processorExecutor[C]) parseConfig(yamlConfig string) (*C, error) { 72 | deserializedYaml, err := confmap.NewRetrievedFromYAML([]byte(yamlConfig)) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | deserializedConf, err := deserializedYaml.AsConf() 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | configMap := make(map[string]any) 83 | for k, v := range deserializedConf.ToStringMap() { 84 | configMap[k] = escapeDollarSigns(v) 85 | } 86 | 87 | defaultConfig := p.factory.CreateDefaultConfig().(*C) 88 | err = confmap.NewFromStringMap(configMap).Unmarshal(&defaultConfig) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | return defaultConfig, nil 94 | } 95 | 96 | func escapeDollarSigns(val any) any { 97 | switch v := val.(type) { 98 | case string: 99 | return strings.ReplaceAll(v, "$$", "$") 100 | case []any: 101 | escapedVals := make([]any, len(v)) 102 | for i, x := range v { 103 | escapedVals[i] = escapeDollarSigns(x) 104 | } 105 | return escapedVals 106 | case map[string]any: 107 | escapedMap := make(map[string]any, len(v)) 108 | for k, x := range v { 109 | escapedMap[k] = escapeDollarSigns(x) 110 | } 111 | return escapedMap 112 | default: 113 | return val 114 | } 115 | } 116 | 117 | func (p *processorExecutor[C]) ExecuteLogStatements(yamlConfig, input string) ([]byte, error) { 118 | config, err := p.parseConfig(yamlConfig) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | transformedLogs := plog.NewLogs() 124 | logsConsumer, _ := consumer.NewLogs(func(_ context.Context, ld plog.Logs) error { 125 | transformedLogs = ld 126 | return nil 127 | }) 128 | 129 | logsProcessor, err := p.factory.CreateLogs(context.Background(), p.settings, config, logsConsumer) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | logsUnmarshaler := &plog.JSONUnmarshaler{} 135 | inputLogs, err := logsUnmarshaler.UnmarshalLogs([]byte(input)) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | err = logsProcessor.ConsumeLogs(context.Background(), inputLogs) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | logsMarshaler := plog.JSONMarshaler{} 146 | json, err := logsMarshaler.MarshalLogs(transformedLogs) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | return json, nil 152 | } 153 | 154 | func (p *processorExecutor[C]) ExecuteTraceStatements(yamlConfig, input string) ([]byte, error) { 155 | config, err := p.parseConfig(yamlConfig) 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | transformedTraces := ptrace.NewTraces() 161 | tracesConsumer, _ := consumer.NewTraces(func(_ context.Context, ld ptrace.Traces) error { 162 | transformedTraces = ld 163 | return nil 164 | }) 165 | 166 | tracesProcessor, err := p.factory.CreateTraces(context.Background(), p.settings, config, tracesConsumer) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | tracesUnmarshaler := &ptrace.JSONUnmarshaler{} 172 | inputTraces, err := tracesUnmarshaler.UnmarshalTraces([]byte(input)) 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | err = tracesProcessor.ConsumeTraces(context.Background(), inputTraces) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | tracesMarshaler := ptrace.JSONMarshaler{} 183 | json, err := tracesMarshaler.MarshalTraces(transformedTraces) 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | return json, nil 189 | } 190 | 191 | func (p *processorExecutor[C]) ExecuteMetricStatements(yamlConfig, input string) ([]byte, error) { 192 | config, err := p.parseConfig(yamlConfig) 193 | if err != nil { 194 | return nil, err 195 | } 196 | 197 | transformedMetrics := pmetric.NewMetrics() 198 | metricsConsumer, _ := consumer.NewMetrics(func(_ context.Context, ld pmetric.Metrics) error { 199 | transformedMetrics = ld 200 | return nil 201 | }) 202 | 203 | metricsProcessor, err := p.factory.CreateMetrics(context.Background(), p.settings, config, metricsConsumer) 204 | if err != nil { 205 | return nil, err 206 | } 207 | 208 | tracesUnmarshaler := &pmetric.JSONUnmarshaler{} 209 | inputMetrics, err := tracesUnmarshaler.UnmarshalMetrics([]byte(input)) 210 | if err != nil { 211 | return nil, err 212 | } 213 | 214 | err = metricsProcessor.ConsumeMetrics(context.Background(), inputMetrics) 215 | if err != nil { 216 | return nil, err 217 | } 218 | 219 | metricsMarshaler := pmetric.JSONMarshaler{} 220 | json, err := metricsMarshaler.MarshalMetrics(transformedMetrics) 221 | if err != nil { 222 | return nil, err 223 | } 224 | 225 | return json, nil 226 | } 227 | 228 | func (p *processorExecutor[C]) ExecuteProfileStatements(yamlConfig, input string) ([]byte, error) { 229 | config, err := p.parseConfig(yamlConfig) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | factory, ok := p.factory.(xprocessor.Factory) 235 | if !ok { 236 | return nil, errors.New("profiles are not supported by this OTel Collector version or component") 237 | } 238 | 239 | transformedProfiles := pprofile.NewProfiles() 240 | metricsConsumer, _ := xconsumer.NewProfiles(func(_ context.Context, ld pprofile.Profiles) error { 241 | transformedProfiles = ld 242 | return nil 243 | }) 244 | 245 | profilesProcessor, err := factory.CreateProfiles(context.Background(), p.settings, config, metricsConsumer) 246 | if err != nil { 247 | return nil, err 248 | } 249 | 250 | profilesUnmarshaler := &pprofile.JSONUnmarshaler{} 251 | inputProfiles, err := profilesUnmarshaler.UnmarshalProfiles([]byte(input)) 252 | if err != nil { 253 | return nil, err 254 | } 255 | 256 | err = profilesProcessor.ConsumeProfiles(context.Background(), inputProfiles) 257 | if err != nil { 258 | return nil, err 259 | } 260 | 261 | profilesMarshaler := pprofile.JSONMarshaler{} 262 | json, err := profilesMarshaler.MarshalProfiles(transformedProfiles) 263 | if err != nil { 264 | return nil, err 265 | } 266 | 267 | return json, nil 268 | } 269 | 270 | func (p *processorExecutor[C]) ObservedLogs() *ObservedLogs { 271 | return p.observedLogs 272 | } 273 | -------------------------------------------------------------------------------- /web/src/components/navbar/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import {css, html, LitElement} from 'lit-element'; 21 | import {nothing} from 'lit'; 22 | 23 | export class PlaygroundNavBar extends LitElement { 24 | static properties = { 25 | title: {type: String}, 26 | githubLink: {type: String, attribute: 'github-link'}, 27 | }; 28 | 29 | constructor() { 30 | super(); 31 | this.title = 'OTTL Playground'; 32 | } 33 | 34 | static get styles() { 35 | return css` 36 | :host .navbar { 37 | width: 100%; 38 | overflow: auto; 39 | display: flex; 40 | background-color: var(--background-color, rgb(79, 98, 173)); 41 | border-bottom: 2px solid var(--border-bottom-color, #333f70); 42 | } 43 | 44 | .navbar a { 45 | text-align: center; 46 | padding: 10px 3px 10px 10px; 47 | color: white; 48 | text-decoration: none; 49 | font-size: 17px; 50 | } 51 | 52 | .navbar a .logo svg { 53 | height: 30px; 54 | } 55 | 56 | @media screen and (max-width: 500px) { 57 | .navbar a { 58 | float: none; 59 | display: block; 60 | } 61 | } 62 | 63 | .github-corner:hover .octo-arm { 64 | animation: octocat-wave 560ms ease-in-out; 65 | } 66 | 67 | @keyframes octocat-wave { 68 | 0%, 69 | 100% { 70 | transform: rotate(0); 71 | } 72 | 20%, 73 | 60% { 74 | transform: rotate(-25deg); 75 | } 76 | 40%, 77 | 80% { 78 | transform: rotate(10deg); 79 | } 80 | } 81 | 82 | @media (max-width: 500px) { 83 | .github-corner:hover .octo-arm { 84 | animation: none; 85 | } 86 | .github-corner .octo-arm { 87 | animation: octocat-wave 560ms ease-in-out; 88 | } 89 | } 90 | 91 | .title-beta-box { 92 | font-size: 7px !important; 93 | font-weight: 300 !important; 94 | color: white; 95 | border: gray solid 1px; 96 | padding-left: 2px; 97 | padding-right: 2px; 98 | margin-top: -8px; 99 | cursor: default; 100 | } 101 | 102 | .title { 103 | display: flex; 104 | color: white; 105 | align-items: center; 106 | gap: 3px; 107 | width: 100%; 108 | } 109 | 110 | .title .text { 111 | font-weight: 500; 112 | cursor: default; 113 | } 114 | 115 | .github-link-container { 116 | height: 100%; 117 | width: 100%; 118 | display: flex; 119 | justify-content: flex-end; 120 | } 121 | `; 122 | } 123 | 124 | render() { 125 | return html` 126 | 170 | `; 171 | } 172 | } 173 | 174 | customElements.define('playground-navbar', PlaygroundNavBar); 175 | -------------------------------------------------------------------------------- /web/src/components/controls/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import {css, html, LitElement} from 'lit-element'; 21 | import {globalStyles} from '../../styles.js'; 22 | import {repeat} from 'lit/directives/repeat.js'; 23 | import {nothing} from 'lit'; 24 | import './copy-link-button'; 25 | 26 | export class PlaygroundControls extends LitElement { 27 | static properties = { 28 | title: {type: String}, 29 | evaluator: {type: String}, 30 | evaluators: {type: Object}, 31 | version: {type: String}, 32 | versions: {type: Object}, 33 | hideEvaluators: {type: Boolean, attribute: 'hide-evaluators'}, 34 | hideRunButton: {type: Boolean, attribute: 'hide-run-button'}, 35 | hideCopyLinkButton: {type: Boolean, attribute: 'hide-copy-link-button'}, 36 | loading: {type: Boolean}, 37 | }; 38 | 39 | constructor() { 40 | super(); 41 | this.hideEvaluators = false; 42 | this.hideRunButton = false; 43 | this.hideCopyLinkButton = false; 44 | this.loading = false; 45 | this.evaluator = 'transform_processor'; 46 | this.versions = [{version: '-', artifact: 'ottlplayground.wasm'}]; 47 | this.evaluators = [ 48 | {id: 'transform_processor', name: 'Transform processor'}, 49 | {id: 'filter_processor', name: 'Filter processor'}, 50 | ]; 51 | 52 | window.addEventListener('keydown', (event) => { 53 | if (event.shiftKey && event.key === 'Enter') { 54 | this._notifyRunRequested(); 55 | } 56 | }); 57 | } 58 | 59 | static get styles() { 60 | return [ 61 | css` 62 | .playground-controls { 63 | overflow: hidden; 64 | width: 100%; 65 | } 66 | 67 | .playground-controls div { 68 | float: left; 69 | text-align: center; 70 | margin: 5px 0 5px 0; 71 | text-decoration: none; 72 | } 73 | 74 | .playground-controls .app-title { 75 | font-size: 25px; 76 | padding: 10px; 77 | font-weight: 600; 78 | } 79 | 80 | .playground-controls div.right { 81 | float: right; 82 | display: flex; 83 | align-items: center; 84 | justify-content: center; 85 | 86 | div:not(:last-child) { 87 | margin-right: 4px; 88 | } 89 | } 90 | 91 | .run-button { 92 | background-color: #04aa6d; 93 | border: 1px solid #049d65; 94 | color: white; 95 | padding: 10px; 96 | width: 75px; 97 | text-align: center; 98 | text-decoration: none; 99 | display: inline-block; 100 | font-size: 16px; 101 | cursor: pointer; 102 | border-radius: 4px; 103 | } 104 | 105 | .run-button:hover { 106 | background-color: #3e8e41; 107 | } 108 | 109 | #evaluator { 110 | width: 160px; 111 | height: 40px; 112 | } 113 | 114 | #version { 115 | height: 40px; 116 | } 117 | `, 118 | globalStyles, 119 | ]; 120 | } 121 | 122 | render() { 123 | return html` 124 |
125 | 126 |
127 | ${this.title} 128 |
129 |
130 |
131 | ${this.hideEvaluators 132 | ? nothing 133 | : html` 134 |
135 | 159 |
160 |
161 | 183 |
184 | `} 185 | ${this.hideRunButton 186 | ? nothing 187 | : html` 188 |
189 | 214 |
215 | `} 216 | ${this.hideCopyLinkButton 217 | ? nothing 218 | : html` 219 | 220 | `} 221 | 222 |
223 |
224 | `; 225 | } 226 | 227 | _notifyRunRequested() { 228 | this.dispatchEvent( 229 | new Event('playground-run-requested', {bubbles: true, composed: true}) 230 | ); 231 | } 232 | 233 | _notifyVersionChanged(e) { 234 | const event = new CustomEvent('version-changed', { 235 | detail: { 236 | value: e.target.value, 237 | }, 238 | bubbles: true, 239 | composed: true, 240 | }); 241 | this.dispatchEvent(event); 242 | } 243 | 244 | _notifyEvaluatorChanged(e) { 245 | const event = new CustomEvent('evaluator-changed', { 246 | detail: { 247 | value: e.target.value, 248 | }, 249 | bubbles: true, 250 | composed: true, 251 | }); 252 | this.dispatchEvent(event); 253 | } 254 | } 255 | 256 | customElements.define('playground-controls', PlaygroundControls); 257 | -------------------------------------------------------------------------------- /web/src/components/panels/result-panel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import {html, LitElement} from 'lit-element'; 21 | import * as htmlFormatter from 'jsondiffpatch/formatters/html'; 22 | import {basicSetup, EditorView} from 'codemirror'; 23 | import {json} from '@codemirror/lang-json'; 24 | import * as jsondiffpatch from 'jsondiffpatch'; 25 | import * as annotatedFormatter from 'jsondiffpatch/formatters/annotated'; 26 | import {resultPanelStyles} from './result-panel.styles.js'; 27 | import {escapeHTML} from '../utils/escape-html'; 28 | import {nothing} from 'lit'; 29 | 30 | export class PlaygroundResultPanel extends LitElement { 31 | static properties = { 32 | view: {type: String}, 33 | payload: {type: String}, 34 | result: {type: Object}, 35 | _errored: {state: true}, 36 | }; 37 | 38 | constructor() { 39 | super(); 40 | this.view = 'visual_delta'; 41 | } 42 | 43 | static get styles() { 44 | return resultPanelStyles; 45 | } 46 | 47 | updated(changedProperties) { 48 | if ( 49 | changedProperties.has('result') || 50 | (changedProperties.has('view') && changedProperties.get('view')) 51 | ) { 52 | this._renderResult(); 53 | } 54 | super.updated(changedProperties); 55 | } 56 | 57 | showResult(payload, result) { 58 | this.payload = payload; 59 | this.result = result; 60 | this._renderResult(payload, result); 61 | } 62 | 63 | showErrorMessage(message) { 64 | this.result = null; 65 | this._errored = true; 66 | this._renderResultText(message); 67 | } 68 | 69 | render() { 70 | return html` 71 | ${ 72 | this._errored 73 | ? html` 74 | 79 | ` 80 | : nothing 81 | } 82 |
83 |
84 |
85 | Result 86 |
87 | ${ 88 | this.result?.executionTime !== undefined 89 | ? html` 90 |
94 | 95 | ${this.result?.executionTime} ms 98 | 99 |
100 | ` 101 | : nothing 102 | } 103 |
104 |
105 |
106 |
107 | View 108 |
109 |
110 | 119 |
120 |
121 | 123 |
124 | Show unchanged 125 |
126 | 127 |
128 | 136 |
137 |
138 |
139 |
140 |
141 | `; 142 | } 143 | 144 | _showWrapLinesOption() { 145 | return this.view && (this.view === 'json' || this.view === 'logs'); 146 | } 147 | 148 | _selectedViewChanged(event) { 149 | this.view = event.target.value; 150 | 151 | this.shadowRoot.querySelector('#show-unchanged-group').style.display = 152 | this.view !== 'visual_delta' ? 'none' : ''; 153 | 154 | this.shadowRoot.querySelector('#wrap-lines-group').style.display = 155 | this._showWrapLinesOption() ? '' : 'none'; 156 | } 157 | 158 | _showUnchangedInputChanged(e) { 159 | let el = this._showUnchangedInput(); 160 | if (el.disabled) { 161 | return; 162 | } 163 | if (typeof e.target.checked === 'undefined') { 164 | el.checked = !el.checked; 165 | } 166 | htmlFormatter.showUnchanged( 167 | el.checked, 168 | this.shadowRoot.querySelector('#result-panel') 169 | ); 170 | } 171 | 172 | _wrapLinesInputChanged(e) { 173 | let el = this._wrapLinesInput(); 174 | if (el.disabled) { 175 | return; 176 | } 177 | if (typeof e.target.checked === 'undefined') { 178 | el.checked = !el.checked; 179 | } 180 | this._renderResult(); 181 | } 182 | 183 | _resultPanel() { 184 | return this.shadowRoot.querySelector('#result-panel'); 185 | } 186 | 187 | _showUnchangedInput() { 188 | return this.shadowRoot.querySelector('#show-unchanged-input'); 189 | } 190 | 191 | _wrapLinesInput() { 192 | return this.shadowRoot?.querySelector('#wrap-lines-input'); 193 | } 194 | 195 | _renderResult() { 196 | if (!this.result) return; 197 | 198 | this._resultPanel().innerHTML = ''; 199 | this._errored = !!this.result.error; 200 | 201 | if (this.view === 'logs') { 202 | this._renderExecutionLogsResult(); 203 | return; 204 | } 205 | 206 | let resultError = this.result?.error; 207 | if (resultError) { 208 | this._renderResultText(resultError); 209 | } else { 210 | this._renderJsonDiffResult(); 211 | } 212 | } 213 | 214 | _renderExecutionLogsResult() { 215 | let extensions = [basicSetup, EditorView.editable.of(false), json()]; 216 | 217 | if (this._wrapLinesInput()?.checked) { 218 | extensions.push(EditorView.lineWrapping); 219 | } 220 | 221 | let editor = new EditorView({ 222 | extensions: extensions, 223 | parent: this._resultPanel(), 224 | }); 225 | 226 | editor.dispatch({ 227 | changes: { 228 | from: 0, 229 | to: editor.state.doc.length, 230 | insert: this.result.logs, 231 | }, 232 | }); 233 | } 234 | 235 | _renderJsonDiffResult() { 236 | if (!this.result.value) { 237 | this._renderResultText('Empty result'); 238 | return; 239 | } 240 | 241 | let left = JSON.parse(this.payload); 242 | let right = JSON.parse(this.result.value); 243 | if (this.view === 'json') { 244 | let extensions = [basicSetup, EditorView.editable.of(false), json()]; 245 | 246 | if (this._wrapLinesInput()?.checked) { 247 | extensions.push(EditorView.lineWrapping); 248 | } 249 | 250 | let editor = new EditorView({ 251 | extensions: extensions, 252 | parent: this._resultPanel(), 253 | }); 254 | 255 | editor.dispatch({ 256 | changes: { 257 | from: 0, 258 | to: editor.state.doc.length, 259 | insert: JSON.stringify(right, null, 2), 260 | }, 261 | }); 262 | return; 263 | } 264 | 265 | // Comparable JSON results 266 | const delta = jsondiffpatch 267 | .create({ 268 | objectHash: function (obj, index) { 269 | // Spans 270 | if (obj?.spanId && obj?.traceId) { 271 | return `${obj.spanId}-${obj?.traceId}`; 272 | } 273 | // Metrics 274 | if ( 275 | obj?.name && 276 | (obj?.gauge || 277 | obj?.sum || 278 | obj?.histogram || 279 | obj?.exponentialHistogram || 280 | obj?.summary) 281 | ) { 282 | return obj?.name; 283 | } 284 | // Attributes 285 | if (obj?.key && obj?.value) { 286 | return obj.key; 287 | } 288 | return '$$index:' + index; 289 | }, 290 | }) 291 | .diff(left, right); 292 | if (!delta) { 293 | this._renderResultText('No changes'); 294 | return; 295 | } 296 | 297 | let formatter = 298 | this.view === 'annotated_delta' ? annotatedFormatter : htmlFormatter; 299 | 300 | if (formatter.showUnchanged && formatter.hideUnchanged) { 301 | formatter.showUnchanged( 302 | this._showUnchangedInput().checked, 303 | this._resultPanel() 304 | ); 305 | } 306 | this._resultPanel().innerHTML = formatter.format(delta, left); 307 | } 308 | 309 | _renderResultText(text) { 310 | let resultPanel = this._resultPanel(); 311 | resultPanel.innerHTML = `
${escapeHTML(text)}
`; 312 | } 313 | } 314 | 315 | customElements.define('playground-result-panel', PlaygroundResultPanel); 316 | -------------------------------------------------------------------------------- /web/src/components/panels/result-panel.styles.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import {css} from 'lit-element'; 21 | import {globalStyles} from '../../styles'; 22 | 23 | const resultPanelStyle = css` 24 | .view-select { 25 | padding: 5px; 26 | } 27 | 28 | .result-panel-controls { 29 | overflow: hidden; 30 | flex-wrap: nowrap; 31 | border-left: darkgreen 4px solid; 32 | border-top: #eee 1px solid; 33 | border-bottom: #eee 1px solid; 34 | border-right: #eee 1px solid; 35 | } 36 | 37 | .result-panel-controls div { 38 | float: left; 39 | text-align: center; 40 | margin: 5px 5px 5px 5px; 41 | text-decoration: none; 42 | font-size: 17px; 43 | } 44 | 45 | .result-panel-controls div.right { 46 | float: right; 47 | overflow: hidden; 48 | 49 | div:not(:last-child) { 50 | margin-right: 4px; 51 | } 52 | } 53 | 54 | .result-panel-controls .execution-time-header { 55 | float: right; 56 | font-size: 12px; 57 | color: gray; 58 | cursor: default; 59 | } 60 | 61 | .result-panel-content { 62 | overflow: auto; 63 | height: calc(100% - 74px); 64 | border: #eee 1px solid; 65 | padding-top: 2px; 66 | } 67 | 68 | .result-panel-content .text { 69 | font-size: smaller; 70 | margin-left: 4px; 71 | } 72 | 73 | .result-panel-view { 74 | display: grid; 75 | grid-template-columns: repeat(auto-fill, 135px); 76 | align-items: center; 77 | justify-content: start; 78 | border-left: gray 4px solid; 79 | padding: 5px; 80 | gap: 10px; 81 | font-size: 12px; 82 | overflow: auto; 83 | } 84 | 85 | .result-panel-flex-group { 86 | display: flex; 87 | align-items: center; 88 | cursor: default; 89 | } 90 | 91 | .cm-editor { 92 | height: calc(100%); 93 | } 94 | 95 | .cm-scroller { 96 | overflow: auto; 97 | } 98 | `; 99 | 100 | const jsondiffpatchStyle = css` 101 | /* 102 | The MIT License 103 | 104 | Copyright (c) 2018 Benjamin Eidelman, https://twitter.com/beneidel 105 | 106 | Permission is hereby granted, free of charge, to any person obtaining a copy 107 | of this software and associated documentation files (the "Software"), to deal 108 | in the Software without restriction, including without limitation the rights 109 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 110 | copies of the Software, and to permit persons to whom the Software is 111 | furnished to do so, subject to the following conditions: 112 | 113 | The above copyright notice and this permission notice shall be included in 114 | all copies or substantial portions of the Software. 115 | 116 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 117 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 118 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 119 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 120 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 121 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 122 | THE SOFTWARE. 123 | 124 | https://github.com/benjamine/jsondiffpatch/tree/master/packages/jsondiffpatch/src/formatters/styles 125 | */ 126 | 127 | .jsondiffpatch-delta { 128 | font-family: 129 | 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Courier, monospace; 130 | font-size: 12px; 131 | margin: 0; 132 | padding: 0 0 0 12px; 133 | display: inline-block; 134 | } 135 | 136 | .jsondiffpatch-delta pre { 137 | font-family: 138 | 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Courier, monospace; 139 | font-size: 12px; 140 | margin: 0; 141 | padding: 0; 142 | display: inline-block; 143 | } 144 | 145 | ul.jsondiffpatch-delta { 146 | list-style-type: none; 147 | padding: 0 0 0 20px; 148 | margin: 0; 149 | } 150 | 151 | .jsondiffpatch-delta ul { 152 | list-style-type: none; 153 | padding: 0 0 0 20px; 154 | margin: 0; 155 | } 156 | 157 | .jsondiffpatch-added .jsondiffpatch-property-name, 158 | .jsondiffpatch-added .jsondiffpatch-value pre, 159 | .jsondiffpatch-modified .jsondiffpatch-right-value pre, 160 | .jsondiffpatch-textdiff-added { 161 | background: #bbffbb; 162 | } 163 | 164 | .jsondiffpatch-deleted .jsondiffpatch-property-name, 165 | .jsondiffpatch-deleted pre, 166 | .jsondiffpatch-modified .jsondiffpatch-left-value pre, 167 | .jsondiffpatch-textdiff-deleted { 168 | background: #ffbbbb; 169 | text-decoration: line-through; 170 | } 171 | 172 | .jsondiffpatch-unchanged, 173 | .jsondiffpatch-movedestination { 174 | color: gray; 175 | } 176 | 177 | .jsondiffpatch-unchanged, 178 | .jsondiffpatch-movedestination > .jsondiffpatch-value { 179 | transition: all 0.5s; 180 | -webkit-transition: all 0.5s; 181 | overflow-y: hidden; 182 | } 183 | 184 | .jsondiffpatch-unchanged-showing .jsondiffpatch-unchanged, 185 | .jsondiffpatch-unchanged-showing 186 | .jsondiffpatch-movedestination 187 | > .jsondiffpatch-value { 188 | max-height: 100px; 189 | } 190 | 191 | .jsondiffpatch-unchanged-hidden .jsondiffpatch-unchanged, 192 | .jsondiffpatch-unchanged-hidden 193 | .jsondiffpatch-movedestination 194 | > .jsondiffpatch-value { 195 | max-height: 0; 196 | } 197 | 198 | .jsondiffpatch-unchanged-hiding 199 | .jsondiffpatch-movedestination 200 | > .jsondiffpatch-value, 201 | .jsondiffpatch-unchanged-hidden 202 | .jsondiffpatch-movedestination 203 | > .jsondiffpatch-value { 204 | display: block; 205 | } 206 | 207 | .jsondiffpatch-unchanged-visible .jsondiffpatch-unchanged, 208 | .jsondiffpatch-unchanged-visible 209 | .jsondiffpatch-movedestination 210 | > .jsondiffpatch-value { 211 | max-height: 100px; 212 | } 213 | 214 | .jsondiffpatch-unchanged-hiding .jsondiffpatch-unchanged, 215 | .jsondiffpatch-unchanged-hiding 216 | .jsondiffpatch-movedestination 217 | > .jsondiffpatch-value { 218 | max-height: 0; 219 | } 220 | 221 | .jsondiffpatch-unchanged-showing .jsondiffpatch-arrow, 222 | .jsondiffpatch-unchanged-hiding .jsondiffpatch-arrow { 223 | display: none; 224 | } 225 | 226 | .jsondiffpatch-value { 227 | display: inline-block; 228 | } 229 | 230 | .jsondiffpatch-property-name { 231 | display: inline-block; 232 | padding-right: 5px; 233 | vertical-align: top; 234 | } 235 | 236 | .jsondiffpatch-property-name:after { 237 | content: ': '; 238 | } 239 | 240 | .jsondiffpatch-child-node-type-array > .jsondiffpatch-property-name:after { 241 | content: ': ['; 242 | } 243 | 244 | .jsondiffpatch-child-node-type-array:after { 245 | content: '],'; 246 | } 247 | 248 | div.jsondiffpatch-child-node-type-array:before { 249 | content: '['; 250 | } 251 | 252 | div.jsondiffpatch-child-node-type-array:after { 253 | content: ']'; 254 | } 255 | 256 | .jsondiffpatch-child-node-type-object > .jsondiffpatch-property-name:after { 257 | content: ': {'; 258 | } 259 | 260 | .jsondiffpatch-child-node-type-object:after { 261 | content: '},'; 262 | } 263 | 264 | div.jsondiffpatch-child-node-type-object:before { 265 | content: '{'; 266 | } 267 | 268 | div.jsondiffpatch-child-node-type-object:after { 269 | content: '}'; 270 | } 271 | 272 | .jsondiffpatch-value pre:after { 273 | content: ','; 274 | } 275 | 276 | li:last-child > .jsondiffpatch-value pre:after, 277 | .jsondiffpatch-modified > .jsondiffpatch-left-value pre:after { 278 | content: ''; 279 | } 280 | 281 | .jsondiffpatch-modified .jsondiffpatch-value { 282 | display: inline-block; 283 | } 284 | 285 | .jsondiffpatch-modified .jsondiffpatch-right-value { 286 | margin-left: 5px; 287 | } 288 | 289 | .jsondiffpatch-moved .jsondiffpatch-value { 290 | display: none; 291 | } 292 | 293 | .jsondiffpatch-moved .jsondiffpatch-moved-destination { 294 | display: inline-block; 295 | background: #ffffbb; 296 | color: #888; 297 | } 298 | 299 | .jsondiffpatch-moved .jsondiffpatch-moved-destination:before { 300 | content: ' => '; 301 | } 302 | 303 | ul.jsondiffpatch-textdiff { 304 | padding: 0; 305 | } 306 | 307 | .jsondiffpatch-textdiff-location { 308 | color: #bbb; 309 | display: inline-block; 310 | min-width: 60px; 311 | } 312 | 313 | .jsondiffpatch-textdiff-line { 314 | display: inline-block; 315 | } 316 | 317 | .jsondiffpatch-textdiff-line-number:after { 318 | content: ','; 319 | } 320 | 321 | .jsondiffpatch-error { 322 | background: red; 323 | color: white; 324 | font-weight: bold; 325 | } 326 | 327 | /* */ 328 | 329 | .jsondiffpatch-annotated-delta { 330 | font-family: 331 | 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Courier, monospace; 332 | font-size: 12px; 333 | margin: 0; 334 | padding: 0 0 0 12px; 335 | display: inline-block; 336 | } 337 | 338 | .jsondiffpatch-annotated-delta pre { 339 | font-family: 340 | 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Courier, monospace; 341 | font-size: 12px; 342 | margin: 0; 343 | padding: 0; 344 | display: inline-block; 345 | } 346 | 347 | .jsondiffpatch-annotated-delta td { 348 | margin: 0; 349 | padding: 0; 350 | } 351 | 352 | .jsondiffpatch-annotated-delta td pre:hover { 353 | font-weight: bold; 354 | } 355 | 356 | td.jsondiffpatch-delta-note { 357 | font-style: italic; 358 | padding-left: 10px; 359 | } 360 | 361 | .jsondiffpatch-delta-note > div { 362 | margin: 0; 363 | padding: 0; 364 | } 365 | 366 | .jsondiffpatch-delta-note pre { 367 | font-style: normal; 368 | } 369 | 370 | .jsondiffpatch-annotated-delta .jsondiffpatch-delta-note { 371 | color: #777; 372 | } 373 | 374 | .jsondiffpatch-annotated-delta tr:hover { 375 | background: #ffc; 376 | } 377 | 378 | .jsondiffpatch-annotated-delta tr:hover > td.jsondiffpatch-delta-note { 379 | color: black; 380 | } 381 | 382 | .jsondiffpatch-error { 383 | background: red; 384 | color: white; 385 | font-weight: bold; 386 | } 387 | `; 388 | 389 | export const resultPanelStyles = [ 390 | resultPanelStyle, 391 | jsondiffpatchStyle, 392 | globalStyles, 393 | ]; 394 | -------------------------------------------------------------------------------- /web/src/components/examples.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | export const PAYLOAD_EXAMPLES = { 21 | logs: '{"resourceLogs":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"my.service"}}]},"scopeLogs":[{"scope":{"name":"my.library","version":"1.0.0","attributes":[{"key":"my.scope.attribute","value":{"stringValue":"some scope attribute"}}]},"logRecords":[{"timeUnixNano":"1544712660300000000","observedTimeUnixNano":"1544712660300000000","severityNumber":10,"severityText":"Information","traceId":"5b8efff798038103d269b633813fc60c","spanId":"eee19b7ec3c1b174","body":{"stringValue":"Example log record"},"attributes":[{"key":"string.attribute","value":{"stringValue":"some string"}},{"key":"boolean.attribute","value":{"boolValue":true}},{"key":"int.attribute","value":{"intValue":"10"}},{"key":"double.attribute","value":{"doubleValue":637.704}},{"key":"array.attribute","value":{"arrayValue":{"values":[{"stringValue":"many"},{"stringValue":"values"}]}}},{"key":"map.attribute","value":{"kvlistValue":{"values":[{"key":"some.map.key","value":{"stringValue":"some value"}}]}}}]}]}]}]}', 22 | traces: 23 | '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"my.service"}}]},"scopeSpans":[{"scope":{"name":"my.library","version":"1.0.0","attributes":[{"key":"my.scope.attribute","value":{"stringValue":"some scope attribute"}}]},"spans":[{"traceId":"5b8efff798038103d269b633813fc60c","spanId":"eee19b7ec3c1b174","parentSpanId":"eee19b7ec3c1b173","name":"I\'m a server span","startTimeUnixNano":"1544712660000000000","endTimeUnixNano":"1544712661000000000","kind":2,"attributes":[{"key":"my.span.attr","value":{"stringValue":"some value"}}],"status":{}},{"traceId":"5b8efff798038103d269b633813fc60c","spanId":"eee19b7ec3c1b173","parentSpanId":"eee19b7ec3c1b173","name":"Me too","startTimeUnixNano":"1544712660000000000","endTimeUnixNano":"1544712661000000000","kind":1,"attributes":[{"key":"my.span.attr","value":{"stringValue":"some value"}}],"status":{}}]}]}]}', 24 | metrics: 25 | '{"resourceMetrics":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"my.service"}},{"key":"timestamp","value":{"stringValue":"2018-12-01T16:17:18Z"}}]},"scopeMetrics":[{"scope":{"name":"my.library","version":"1.0.0","attributes":[{"key":"my.scope.attribute","value":{"stringValue":"some scope attribute"}}]},"metrics":[{"name":"my.counter","unit":"1","description":"I am a Counter","sum":{"aggregationTemporality":1,"isMonotonic":true,"dataPoints":[{"asDouble":5,"startTimeUnixNano":"1544712660300000000","timeUnixNano":"1544712660300000000","attributes":[{"key":"my.counter.attr","value":{"stringValue":"some value"}}]},{"asDouble":2,"startTimeUnixNano":"1544712660300000000","timeUnixNano":"1544712660300000000","attributes":[{"key":"another.counter.attr","value":{"stringValue":"another value"}}]}]}},{"name":"my.gauge","unit":"1","description":"I am a Gauge","gauge":{"dataPoints":[{"asDouble":10,"timeUnixNano":"1544712660300000000","attributes":[{"key":"my.gauge.attr","value":{"stringValue":"some value"}}]}]}},{"name":"my.histogram","unit":"1","description":"I am a Histogram","histogram":{"aggregationTemporality":1,"dataPoints":[{"startTimeUnixNano":"1544712660300000000","timeUnixNano":"1544712660300000000","count":"2","sum":2,"bucketCounts":["1","1"],"explicitBounds":[1],"min":0,"max":2,"attributes":[{"key":"my.histogram.attr","value":{"stringValue":"some value"}}]}]}}]}]}]}', 26 | }; 27 | 28 | const TRANSFORM_PROCESSOR_CONFIG_EXAMPLES = [ 29 | { 30 | name: 'Rename an attribute', 31 | otlp_type: 'traces', 32 | config: 33 | 'error_mode: ignore\n' + 34 | 'trace_statements:\n' + 35 | ' - context: resource\n' + 36 | ' statements:\n' + 37 | ' - set(attributes["service.new_name"], attributes["service.name"])\n' + 38 | ' - delete_key(attributes, "service.name")', 39 | }, 40 | { 41 | name: 'Copy field to attributes', 42 | otlp_type: 'logs', 43 | config: 44 | 'error_mode: ignore\n' + 45 | 'log_statements:\n' + 46 | ' - context: log\n' + 47 | ' statements:\n' + 48 | ' - set(attributes["body"], body)', 49 | }, 50 | { 51 | name: 'Combine two attributes', 52 | otlp_type: 'logs', 53 | config: 54 | 'error_mode: ignore\n' + 55 | 'log_statements:\n' + 56 | ' - context: log\n' + 57 | ' statements:\n' + 58 | ' - set(attributes["combined"], Concat([attributes["string.attribute"], attributes["boolean.attribute"]], " "))', 59 | }, 60 | { 61 | name: 'Set a field', 62 | otlp_type: 'logs', 63 | config: 64 | 'log_statements:\n' + 65 | ' - context: log\n' + 66 | ' statements:\n' + 67 | ' - set(severity_number, SEVERITY_NUMBER_INFO)\n' + 68 | ' - set(severity_text, "INFO")', 69 | }, 70 | { 71 | name: 'Parse unstructured log', 72 | otlp_type: 'logs', 73 | config: 74 | 'log_statements:\n' + 75 | ' - context: log\n' + 76 | ' statements:\n' + 77 | ' - \'merge_maps(attributes, ExtractPatterns(body, "Example (?P[a-z\\\\.]+)"), "upsert")\'', 78 | }, 79 | { 80 | name: 'Conditionally set a field', 81 | otlp_type: 'traces', 82 | config: 83 | 'trace_statements:\n' + 84 | ' - context: span\n' + 85 | ' statements:\n' + 86 | ' - set(attributes["server"], true) where kind == SPAN_KIND_SERVER', 87 | }, 88 | { 89 | name: 'Update a resource attribute', 90 | otlp_type: 'logs', 91 | config: 92 | 'log_statements:\n' + 93 | ' - context: resource\n' + 94 | ' statements:\n' + 95 | ' - set(attributes["service.name"], "mycompany-application") ', 96 | }, 97 | { 98 | name: 'Parse and manipulate JSON', 99 | otlp_type: 'logs', 100 | config: 101 | 'log_statements:\n' + 102 | ' - context: log\n' + 103 | ' statements:\n' + 104 | ' - merge_maps(cache, ParseJSON(body), "upsert") where IsMatch(body, "^\\\\{")\n' + 105 | ' - set(time, Time(cache["timestamp"], "%Y-%m-%dT%H:%M:%SZ"))\n' + 106 | ' - set(severity_text, cache["level"])\n' + 107 | ' - set(body, cache["message"])', 108 | payload: 109 | '{"resourceLogs":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"my.service"}}]},"scopeLogs":[{"scope":{"name":"my.library","version":"1.0.0","attributes":[{"key":"my.scope.attribute","value":{"stringValue":"some scope attribute"}}]},"logRecords":[{"timeUnixNano":"1544712660300000000","observedTimeUnixNano":"1544712660300000000","severityNumber":10,"traceId":"5b8efff798038103d269b633813fc60c","spanId":"eee19b7ec3c1b174","body":{"stringValue":"{\\"timestamp\\": \\"2025-03-01T12:12:14Z\\", \\"level\\":\\"INFO\\",\\"message\\":\\"Elapsed time: 10ms\\"}"}}]}]}]}', 110 | }, 111 | { 112 | name: 'Parse and manipulate Timestamps', 113 | otlp_type: 'metrics', 114 | config: 115 | 'metric_statements:\n' + 116 | ' - context: resource\n' + 117 | ' statements:\n' + 118 | ' - set(attributes["date"], String(TruncateTime(Time(attributes["timestamp"], "%Y-%m-%dT%H:%M:%SZ"), Duration("24h"))))', 119 | }, 120 | { 121 | name: 'Manipulate strings', 122 | otlp_type: 'metrics', 123 | config: 124 | 'metric_statements:\n' + 125 | ' - context: scope\n' + 126 | ' statements:\n' + 127 | ' - set(resource.attributes["service.name"], ConvertCase(Concat([resource.attributes["service.name"], version], ".v"), "upper"))', 128 | }, 129 | { 130 | name: 'Scale a metric', 131 | otlp_type: 'metrics', 132 | config: 133 | 'metric_statements:\n' + 134 | ' - context: metric\n' + 135 | ' statements:\n' + 136 | ' - scale_metric(10.0, "kWh") where name == "my.gauge"', 137 | }, 138 | { 139 | name: 'Dynamically rename a metric', 140 | otlp_type: 'metrics', 141 | config: 142 | 'metric_statements:\n' + 143 | ' - context: metric\n' + 144 | ' statements:\n' + 145 | ' - replace_pattern(name, "my.(.+)", "metrics.$$1")', 146 | }, 147 | { 148 | name: 'Aggregate a metric', 149 | otlp_type: 'metrics', 150 | config: 151 | 'metric_statements:\n' + 152 | ' - context: metric\n' + 153 | ' statements:\n' + 154 | ' - copy_metric(name="my.second.histogram") where name == "my.histogram"\n' + 155 | ' - aggregate_on_attributes("sum", []) where name == "my.second.histogram"', 156 | }, 157 | ]; 158 | 159 | const FILTER_PROCESSOR_CONFIG_EXAMPLES = [ 160 | { 161 | name: 'Drop specific metric and value', 162 | otlp_type: 'metrics', 163 | config: 164 | 'metrics:\n' + 165 | ' datapoint:\n' + 166 | ' - metric.name == "my.histogram" and count == 2', 167 | }, 168 | { 169 | name: 'Drop spans', 170 | otlp_type: 'traces', 171 | config: 172 | 'error_mode: ignore\n' + 173 | 'traces:\n' + 174 | ' span:\n' + 175 | ' - kind == SPAN_KIND_INTERNAL', 176 | }, 177 | { 178 | name: 'Drop data by resource attribute', 179 | otlp_type: 'traces', 180 | config: 181 | 'traces:\n' + 182 | ' span:\n' + 183 | ' - IsMatch(resource.attributes["service.name"], "my-*")', 184 | }, 185 | { 186 | name: 'Drop debug and trace logs', 187 | otlp_type: 'logs', 188 | config: 189 | 'logs:\n' + 190 | ' log_record:\n' + 191 | ' - severity_number != SEVERITY_NUMBER_UNSPECIFIED and severity_number < SEVERITY_NUMBER_INFO', 192 | payload: 193 | '{"resourceLogs":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"my.service"}}]},"scopeLogs":[{"scope":{"name":"my.library","version":"1.0.0","attributes":[{"key":"my.scope.attribute","value":{"stringValue":"some scope attribute"}}]},"logRecords":[{"timeUnixNano":"1544712660300000000","observedTimeUnixNano":"1544712660300000000","severityNumber":10,"severityText":"Information","traceId":"5b8efff798038103d269b633813fc60c","spanId":"eee19b7ec3c1b174","body":{"stringValue":"I\'m an INFO log record"}},{"timeUnixNano":"1544712660300000000","observedTimeUnixNano":"1544712660300000000","severityNumber":5,"severityText":"Debug","traceId":"5b8efff798038103d269b633813fc60c","spanId":"eee19b7ec3c1b174","body":{"stringValue":"I\'m a DEBUG log record"}}]}]}]}', 194 | }, 195 | ]; 196 | 197 | const sortBy = (property) => { 198 | return function (a, b) { 199 | return a[property] < b[property] ? -1 : a[property] > b[property] ? 1 : 0; 200 | }; 201 | }; 202 | 203 | export const CONFIG_EXAMPLES = { 204 | transform_processor: TRANSFORM_PROCESSOR_CONFIG_EXAMPLES.sort(sortBy('name')), 205 | filter_processor: FILTER_PROCESSOR_CONFIG_EXAMPLES.sort(sortBy('name')), 206 | }; 207 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /ci-tools/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "encoding/json" 24 | "errors" 25 | "flag" 26 | "fmt" 27 | "go/format" 28 | "io" 29 | "net/http" 30 | "os" 31 | "path/filepath" 32 | "slices" 33 | "strconv" 34 | "strings" 35 | 36 | "golang.org/x/mod/modfile" 37 | "golang.org/x/mod/semver" 38 | ) 39 | 40 | type args struct { 41 | version string 42 | unregisteredVersionsCount int 43 | } 44 | 45 | func main() { 46 | currentVersion, err := lookupVersion() 47 | if err != nil { 48 | fmt.Println(err) 49 | } 50 | 51 | commandsArgs := &args{} 52 | 53 | getVersionCmd := flag.NewFlagSet("get-version", flag.ExitOnError) 54 | validateRegisteredVersionsCmd := flag.NewFlagSet("validate-registered-versions", flag.ExitOnError) 55 | 56 | getUnregisteredVersionsCmd := flag.NewFlagSet("get-unregistered-versions", flag.ExitOnError) 57 | getUnregisteredVersionsCmd.IntVar(&commandsArgs.unregisteredVersionsCount, "count", lookupUnregisteredVersionsCount(), 58 | "Number of unregistered versions to list") 59 | 60 | registerWasmCmd := flag.NewFlagSet("register-wasm", flag.ExitOnError) 61 | addVersionFlag(&commandsArgs.version, currentVersion, registerWasmCmd) 62 | 63 | generateConstantsCmd := flag.NewFlagSet("generate-constants", flag.ExitOnError) 64 | addVersionFlag(&commandsArgs.version, currentVersion, generateConstantsCmd) 65 | 66 | generateProcessorsUpdateCmd := flag.NewFlagSet("generate-processors-update", flag.ExitOnError) 67 | addVersionFlag(&commandsArgs.version, currentVersion, generateProcessorsUpdateCmd) 68 | 69 | switch os.Args[1] { 70 | case getVersionCmd.Name(): 71 | version, err := extractProcessorsVersionFromGoModule() 72 | if err != nil { 73 | fmt.Println(err) 74 | } else { 75 | fmt.Println(version) 76 | } 77 | case generateConstantsCmd.Name(): 78 | _ = generateConstantsCmd.Parse(os.Args[2:]) 79 | err = generateVersionsDotGoFile(commandsArgs.version) 80 | if err != nil { 81 | fmt.Println(err) 82 | } else { 83 | fmt.Println(commandsArgs.version) 84 | } 85 | case registerWasmCmd.Name(): 86 | _ = registerWasmCmd.Parse(os.Args[2:]) 87 | err = registerWebAssemblyVersion(commandsArgs.version) 88 | if err != nil { 89 | fmt.Println(err) 90 | } else { 91 | fmt.Println(commandsArgs.version) 92 | } 93 | case generateProcessorsUpdateCmd.Name(): 94 | _ = generateProcessorsUpdateCmd.Parse(os.Args[2:]) 95 | argument, err := generateProcessorsGoGetArgument(commandsArgs.version) 96 | if err != nil { 97 | fmt.Println(err) 98 | } else { 99 | fmt.Println(argument) 100 | } 101 | case getUnregisteredVersionsCmd.Name(): 102 | _ = getUnregisteredVersionsCmd.Parse(os.Args[2:]) 103 | releases, err := getUnregisteredVersions(commandsArgs.unregisteredVersionsCount) 104 | if err != nil { 105 | fmt.Println(err) 106 | } else { 107 | fmt.Println(releases) 108 | } 109 | case validateRegisteredVersionsCmd.Name(): 110 | err = validateWebAssemblyVersions() 111 | if err != nil { 112 | fmt.Println(err.Error()) 113 | os.Exit(1) 114 | } 115 | default: 116 | flag.PrintDefaults() 117 | os.Exit(1) 118 | } 119 | } 120 | 121 | func addVersionFlag(target *string, defaultVal string, cmd *flag.FlagSet) { 122 | cmd.StringVar(target, "version", defaultVal, "opentelemetry-collector-contrib version") 123 | } 124 | 125 | func lookupVersion() (string, error) { 126 | version := os.Getenv("PROCESSORS_VERSION") 127 | if version == "" { 128 | return extractProcessorsVersionFromGoModule() 129 | } 130 | return version, nil 131 | } 132 | 133 | func lookupWasmVersionsJSONPath() string { 134 | path, ok := os.LookupEnv("WASM_VERSIONS_FILE") 135 | if ok { 136 | return path 137 | } 138 | return filepath.Join("web", "public", "wasm", "versions.json") 139 | } 140 | 141 | func lookupUnregisteredVersionsCount() int { 142 | defaultVersionsCount := 10 143 | value, ok := os.LookupEnv("MAX_WASM_PROCESSORS_VERSIONS") 144 | if ok { 145 | intValue, err := strconv.Atoi(value) 146 | if err == nil { 147 | return intValue 148 | } 149 | } 150 | return defaultVersionsCount 151 | } 152 | 153 | func extractProcessorsVersionFromGoModule() (string, error) { 154 | goModFile, err := os.Open("go.mod") 155 | if err != nil { 156 | return "", err 157 | } 158 | 159 | goModFileBytes, err := io.ReadAll(goModFile) 160 | _ = goModFile.Close() 161 | if err != nil { 162 | return "", err 163 | } 164 | 165 | goModInfo, err := modfile.Parse("go.mod", goModFileBytes, nil) 166 | if err != nil { 167 | return "", err 168 | } 169 | 170 | var version string 171 | for _, dep := range goModInfo.Require { 172 | if dep.Indirect { 173 | continue 174 | } 175 | if strings.HasPrefix(dep.Mod.Path, "github.com/open-telemetry/opentelemetry-collector-contrib/processor/") { 176 | if version != "" && version != dep.Mod.Version { 177 | return "", fmt.Errorf("multiple opentelemetry-collector-contrib versions found: %q and %q", version, dep.Mod.Version) 178 | } 179 | version = dep.Mod.Version 180 | } 181 | } 182 | 183 | return version, nil 184 | } 185 | 186 | func registerWebAssemblyVersion(version string) error { 187 | wasmVersionsFilePath := lookupWasmVersionsJSONPath() 188 | wasmVersionsFile, err := os.OpenFile(wasmVersionsFilePath, os.O_RDWR|os.O_CREATE, 0666) 189 | if err != nil { 190 | return err 191 | } 192 | 193 | wasmVersionsFileBytes, err := io.ReadAll(wasmVersionsFile) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | _ = wasmVersionsFile.Close() 199 | 200 | var wasmVersions map[string]any 201 | if len(wasmVersionsFileBytes) == 0 { 202 | wasmVersions = map[string]any{} 203 | } else { 204 | err = json.Unmarshal(wasmVersionsFileBytes, &wasmVersions) 205 | if err != nil { 206 | return err 207 | } 208 | } 209 | 210 | if len(wasmVersions) == 0 { 211 | wasmVersions["versions"] = []any{} 212 | } 213 | 214 | for _, v := range wasmVersions["versions"].([]any) { 215 | if v.(map[string]any)["version"].(string) == version { 216 | return nil 217 | } 218 | } 219 | 220 | wasmName := fmt.Sprintf("wasm/ottlplayground-%s.wasm", version) 221 | wasmVersions["versions"] = append(wasmVersions["versions"].([]any), map[string]any{ 222 | "artifact": wasmName, 223 | "version": version, 224 | }) 225 | 226 | slices.SortFunc(wasmVersions["versions"].([]any), func(a, b any) int { 227 | return semver.Compare(b.(map[string]any)["version"].(string), a.(map[string]any)["version"].(string)) 228 | }) 229 | 230 | modifiedWasmVersions, err := json.MarshalIndent(&wasmVersions, "", " ") 231 | if err != nil { 232 | return err 233 | } 234 | 235 | err = os.WriteFile(wasmVersionsFilePath, modifiedWasmVersions, 0600) 236 | if err != nil { 237 | return err 238 | } 239 | 240 | return nil 241 | } 242 | 243 | func validateWebAssemblyVersions() error { 244 | wasmVersionsFilePath := lookupWasmVersionsJSONPath() 245 | wasmVersionsFileBytes, err := os.ReadFile(wasmVersionsFilePath) 246 | if err != nil { 247 | return fmt.Errorf("file %s not found", wasmVersionsFilePath) 248 | } 249 | 250 | var wasmVersions map[string]any 251 | if len(wasmVersionsFileBytes) == 0 { 252 | return nil 253 | } else { 254 | err = json.Unmarshal(wasmVersionsFileBytes, &wasmVersions) 255 | if err != nil { 256 | return err 257 | } 258 | } 259 | 260 | if len(wasmVersions) == 0 { 261 | return nil 262 | } 263 | 264 | var errorsList strings.Builder 265 | for _, version := range wasmVersions["versions"].([]any) { 266 | artifact := version.(map[string]any)["artifact"] 267 | _, err = os.Stat(fmt.Sprintf("web/public/%s", artifact.(string))) 268 | if err != nil { 269 | errorsList.WriteString(fmt.Sprintf("version %s: artifact not found: %s \n", version.(map[string]any)["version"].(string), artifact)) 270 | } 271 | } 272 | 273 | if errorsList.Len() > 0 { 274 | return errors.New(errorsList.String()) 275 | } 276 | 277 | return nil 278 | } 279 | 280 | func generateProcessorsGoGetArgument(version string) (string, error) { 281 | goModFile, err := os.Open("go.mod") 282 | if err != nil { 283 | return "", err 284 | } 285 | 286 | defer func(goModFile *os.File) { 287 | _ = goModFile.Close() 288 | }(goModFile) 289 | 290 | goModFileBytes, err := io.ReadAll(goModFile) 291 | if err != nil { 292 | return "", err 293 | } 294 | 295 | goModInfo, err := modfile.Parse("go.mod", goModFileBytes, nil) 296 | if err != nil { 297 | return "", err 298 | } 299 | 300 | var argument strings.Builder 301 | seem := map[string]bool{} 302 | for _, dep := range goModInfo.Require { 303 | if dep.Indirect { 304 | continue 305 | } 306 | if !seem[dep.Mod.Path] && strings.HasPrefix(dep.Mod.Path, "github.com/open-telemetry/opentelemetry-collector-contrib/processor/") { 307 | seem[dep.Mod.Path] = true 308 | argument.WriteString(fmt.Sprintf("%s@%s", dep.Mod.Path, version)) 309 | argument.WriteString(" ") 310 | } 311 | } 312 | return argument.String(), nil 313 | } 314 | 315 | func generateVersionsDotGoFile(version string) error { 316 | versionsGoFile, err := os.Create(filepath.Join("internal", "versions.go")) 317 | if err != nil { 318 | return err 319 | } 320 | 321 | defer func(versionsGoFile *os.File) { 322 | _ = versionsGoFile.Close() 323 | }(versionsGoFile) 324 | 325 | content := strings.Builder{} 326 | content.WriteString("/*\n * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one\n * or more contributor license agreements. See the NOTICE file distributed with\n * this work for additional information regarding copyright\n * ownership. Elasticsearch B.V. licenses this file to you under\n * the Apache License, Version 2.0 (the \"License\"); you may\n * not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *\thttp://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied. See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n") 327 | content.WriteString("// Code generated. DO NOT EDIT.\n\n") 328 | content.WriteString("package internal\n\n") 329 | content.WriteString(fmt.Sprintf("const CollectorContribProcessorsVersion = \"%s\" \n", version)) 330 | 331 | formattedSource, err := format.Source([]byte(content.String())) 332 | if err != nil { 333 | return err 334 | } 335 | 336 | _, err = fmt.Fprint(versionsGoFile, string(formattedSource)) 337 | if err != nil { 338 | return err 339 | } 340 | 341 | return nil 342 | } 343 | 344 | func getUnregisteredVersions(maxNumOfVersions int) (string, error) { 345 | wasmVersionsFilePath := lookupWasmVersionsJSONPath() 346 | registeredVersions := map[string]bool{} 347 | 348 | _, err := os.Stat(wasmVersionsFilePath) 349 | if err == nil { 350 | var wasmVersionsFileBytes []byte 351 | wasmVersionsFileBytes, err = os.ReadFile(wasmVersionsFilePath) 352 | if err != nil { 353 | return "", err 354 | } 355 | if len(wasmVersionsFileBytes) > 0 { 356 | var fileContent map[string]any 357 | err = json.Unmarshal(wasmVersionsFileBytes, &fileContent) 358 | if err != nil { 359 | return "", err 360 | } 361 | for _, v := range fileContent["versions"].([]any) { 362 | registeredVersions[v.(map[string]any)["version"].(string)] = true 363 | } 364 | } 365 | } 366 | 367 | tagsRes, err := http.Get("https://api.github.com/repos/open-telemetry/opentelemetry-collector-contrib/releases") 368 | if err != nil { 369 | return "", err 370 | } 371 | 372 | defer func(Body io.ReadCloser) { 373 | _ = Body.Close() 374 | }(tagsRes.Body) 375 | 376 | if tagsRes.StatusCode != http.StatusOK { 377 | return "", fmt.Errorf("failed to get release list. status: %s", tagsRes.Status) 378 | } 379 | 380 | var data []map[string]any 381 | err = json.NewDecoder(tagsRes.Body).Decode(&data) 382 | if err != nil { 383 | return "", err 384 | } 385 | 386 | ignoredVersions := map[string]struct{}{} 387 | for _, ignoredVersion := range strings.Split(os.Getenv("IGNORED_WASM_PROCESSORS_VERSIONS"), " ") { 388 | ignoredVersions[ignoredVersion] = struct{}{} 389 | } 390 | 391 | var newVersions []string 392 | for _, release := range data { 393 | version := release["name"].(string) 394 | if _, ok := ignoredVersions[version]; ok { 395 | continue 396 | } 397 | // versions <= v0.110.0 fails to compile due to some breaking changes 398 | if !registeredVersions[version] && semver.Compare(version, "v0.110.0") > 0 { 399 | newVersions = append(newVersions, version) 400 | } 401 | } 402 | 403 | slices.SortFunc(newVersions, semver.Compare) 404 | 405 | if len(newVersions) > maxNumOfVersions { 406 | newVersions = newVersions[len(newVersions)-maxNumOfVersions:] 407 | } 408 | 409 | return strings.Join(newVersions, " "), nil 410 | } 411 | -------------------------------------------------------------------------------- /web/src/components/playground.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 3 | * or more contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright 5 | * ownership. Elasticsearch B.V. licenses this file to you under 6 | * the Apache License, Version 2.0 (the "License"); you may 7 | * not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import {html, LitElement} from 'lit-element'; 21 | import '../wasm_exec.js'; 22 | import Split from 'split.js'; 23 | import {CONFIG_EXAMPLES, PAYLOAD_EXAMPLES} from './examples'; 24 | import './panels/config-panel'; 25 | import './panels/payload-panel'; 26 | import './panels/result-panel'; 27 | import {playgroundStyles} from './playground.styles'; 28 | import {nothing} from 'lit'; 29 | import {getJsonPayloadType} from './utils/json-payload'; 30 | import {base64ToUtf8, utf8ToBase64} from './utils/base64'; 31 | 32 | export class Playground extends LitElement { 33 | static properties = { 34 | title: {type: String}, 35 | config: {type: String}, 36 | payload: {type: String}, 37 | version: {type: String}, 38 | evaluator: {type: String}, 39 | hideEvaluators: {type: Boolean, attribute: 'hide-evaluators'}, 40 | hideRunButton: {type: Boolean, attribute: 'hide-run-button'}, 41 | disableShareLink: {type: Boolean, attribute: 'disable-share-link'}, 42 | baseUrl: {type: String, attribute: 'base-url'}, 43 | 44 | _loading: {state: true}, 45 | _loadingWasm: {state: true}, 46 | _evaluators: {state: true}, 47 | _evaluatorsDocsURL: {state: true}, 48 | _versions: {state: true}, 49 | _result: {state: true}, 50 | }; 51 | 52 | constructor() { 53 | super(); 54 | this._initDefaultValues(); 55 | this._addEventListeners(); 56 | } 57 | 58 | _initDefaultValues() { 59 | this._loading = true; 60 | this._hideEvaluators = false; 61 | this.hideRunButton = false; 62 | this.disableShareLink = false; 63 | this.payload = '{}'; 64 | this.baseUrl = ''; 65 | } 66 | 67 | static get styles() { 68 | return playgroundStyles; 69 | } 70 | 71 | get state() { 72 | return { 73 | version: this.version, 74 | evaluator: this.evaluator, 75 | payload: this.payload, 76 | config: this.config, 77 | }; 78 | } 79 | 80 | set state(state) { 81 | this.version = state.version; 82 | this.evaluator = state.evaluator; 83 | this.payload = state.payload; 84 | this.config = state.config; 85 | // Reset the payload example dropdown 86 | this._setSelectedPayloadExample(''); 87 | } 88 | 89 | firstUpdated() { 90 | this._spitComponents(); 91 | this._loading = false; 92 | this._fetchWebAssemblyVersions().then(() => { 93 | this._initState(); 94 | }); 95 | } 96 | 97 | willUpdate(changedProperties) { 98 | if (changedProperties.has('_evaluators')) { 99 | this._computeEvaluatorsDocsURL(); 100 | } 101 | super.willUpdate(changedProperties); 102 | } 103 | 104 | _initState() { 105 | let urlStateData = this._loadURLBase64DataHash(); 106 | let version = urlStateData?.version; 107 | if (!version || !this._versions?.some((it) => it.version === version)) { 108 | version = this._versions?.[0]?.version; 109 | } 110 | 111 | if (urlStateData) { 112 | this.state = { 113 | version: version, 114 | evaluator: urlStateData?.evaluator, 115 | payload: urlStateData?.payload ?? '{}', 116 | config: urlStateData?.config, 117 | }; 118 | } 119 | 120 | this._fetchWebAssembly(this._resolveWebAssemblyArtifact(version)); 121 | } 122 | 123 | _fetchWebAssemblyVersions() { 124 | return fetch('wasm/versions.json') 125 | .then((response) => { 126 | return response.json(); 127 | }) 128 | .then((json) => { 129 | this.version = this.version || json.versions?.[0]?.version; 130 | this._versions = json.versions; 131 | }); 132 | } 133 | 134 | _loadURLBase64DataHash() { 135 | if (this.disableShareLink === true) return; 136 | let hash = this._getUrlHash(); 137 | if (hash) { 138 | try { 139 | let data = JSON.parse(base64ToUtf8(hash)); 140 | if (data.payload) { 141 | try { 142 | data.payload = JSON.stringify(JSON.parse(data.payload), null, 2); 143 | } catch (e) { 144 | // Ignore 145 | } 146 | } 147 | return data; 148 | } catch (e) { 149 | return null; 150 | } 151 | } 152 | } 153 | 154 | _setSelectedPayloadExample(example) { 155 | let panel = this.shadowRoot.querySelector('#payload-code-panel'); 156 | if (panel) { 157 | panel.selectedExample = example; 158 | } 159 | } 160 | 161 | _computeEvaluatorsDocsURL() { 162 | let docsURLs = {}; 163 | this._evaluators?.forEach((it) => { 164 | docsURLs[it.id] = it.docsURL ?? null; 165 | }); 166 | this._evaluatorsDocsURL = docsURLs; 167 | } 168 | 169 | render() { 170 | return html` 171 | ${this._loading 172 | ? html` 173 |
174 | 175 | 176 |
177 | ` 178 | : nothing} 179 |
180 | 181 | 193 | 197 | ${this.title 198 | ? html` ${this.title} BETA` 203 | : nothing} 204 | 205 | 209 | 210 | 211 | 212 |
213 |
214 |
215 |
216 | 224 | > 225 | 226 |
227 |
228 | 233 | 234 |
235 |
236 |
237 |
238 | 243 | 244 |
245 |
246 |
247 | `; 248 | } 249 | 250 | _addEventListeners() { 251 | window.addEventListener('playground-wasm-ready', () => { 252 | // eslint-disable-next-line no-undef 253 | this._evaluators = statementsExecutors(); 254 | if (!this._evaluators) { 255 | this.evaluator = ''; 256 | } else { 257 | if (!this.evaluator) { 258 | this.evaluator = this._evaluators[0]?.id; 259 | } else if (!this._evaluators.some((e) => e.id === this.evaluator)) { 260 | this.evaluator = this._evaluators[0]?.id; 261 | } 262 | } 263 | }); 264 | 265 | this.addEventListener('playground-run-requested', () => { 266 | this._runStatements(); 267 | }); 268 | 269 | this.addEventListener('evaluator-changed', (e) => { 270 | this.evaluator = e.detail.value; 271 | }); 272 | 273 | this.addEventListener('version-changed', (e) => { 274 | this.version = e.detail.value; 275 | this._fetchWebAssembly(this._resolveWebAssemblyArtifact(this.version)); 276 | }); 277 | } 278 | 279 | _resolveWebAssemblyArtifact(version) { 280 | return this._versions.find((e) => e.version === version)?.artifact; 281 | } 282 | 283 | _fetchWebAssembly(artifact) { 284 | // eslint-disable-next-line no-undef 285 | const go = new Go(); 286 | this._loadingWasm = true; 287 | 288 | let wasmUrl = this.baseUrl 289 | ? new URL(artifact, this.baseUrl).href 290 | : artifact; 291 | 292 | WebAssembly.instantiateStreaming(fetch(wasmUrl), go.importObject).then( 293 | (result) => { 294 | go.run(result.instance); 295 | this.updateComplete.then(() => { 296 | this._loadingWasm = false; 297 | const event = new CustomEvent('playground-wasm-ready', { 298 | detail: { 299 | value: artifact, 300 | }, 301 | bubbles: true, 302 | composed: true, 303 | cancelable: true, 304 | }); 305 | window.dispatchEvent(event); 306 | }); 307 | } 308 | ); 309 | } 310 | 311 | _spitComponents() { 312 | Split( 313 | [ 314 | this.shadowRoot.querySelector('#config-code-panel-container'), 315 | this.shadowRoot.querySelector('#payload-code-panel-container'), 316 | ], 317 | { 318 | direction: 'vertical', 319 | } 320 | ); 321 | 322 | Split([ 323 | this.shadowRoot.querySelector('#left-panel'), 324 | this.shadowRoot.querySelector('#right-panel'), 325 | ]); 326 | } 327 | 328 | _runStatements() { 329 | let state = this.state; 330 | let payloadType; 331 | try { 332 | payloadType = getJsonPayloadType(this.payload); 333 | } catch (e) { 334 | this.shadowRoot 335 | .querySelector('#result-panel') 336 | .showErrorMessage(`Invalid OTLP JSON payload: ${e}`); 337 | return; 338 | } 339 | 340 | // eslint-disable-next-line no-undef 341 | let result = executeStatements( 342 | state.config, 343 | payloadType, 344 | state.payload, 345 | state.evaluator 346 | ); 347 | 348 | this.dispatchEvent( 349 | new CustomEvent('playground-run-result', { 350 | detail: { 351 | state: state, 352 | result: result, 353 | error: 354 | result && Object.prototype.hasOwnProperty.call(result, 'error'), 355 | }, 356 | bubbles: true, 357 | composed: true, 358 | cancelable: true, 359 | }) 360 | ); 361 | 362 | this.payload = state.payload; 363 | this._result = result; 364 | } 365 | 366 | _handleConfigExampleChanged(event) { 367 | let example = event.detail.value; 368 | if (example) { 369 | let payload = example.payload || PAYLOAD_EXAMPLES[example.otlp_type]; 370 | this.payload = JSON.stringify(JSON.parse(payload), null, 2); 371 | this._setSelectedPayloadExample(example.otlp_type); 372 | } 373 | } 374 | 375 | _handleCopyLinkClick() { 376 | let data = {...this.state}; 377 | try { 378 | // Try to linearize the JSON to make it smaller 379 | data.payload = JSON.stringify(JSON.parse(data.payload)); 380 | } catch (e) { 381 | // Ignore and use it as it's 382 | } 383 | 384 | let key = utf8ToBase64(JSON.stringify(data)); 385 | this._copyToClipboard(this._buildUrlWithLink(key)).catch((e) => { 386 | console.error(e); 387 | }); 388 | 389 | document.location.hash = key; 390 | } 391 | 392 | _buildUrlWithLink(value) { 393 | let urlHash = this._getUrlHash(); 394 | if (urlHash) { 395 | return this._replaceUrlHash(value); 396 | } else { 397 | return this._getCurrentUrl() + '#' + value; 398 | } 399 | } 400 | 401 | _isEmbedded() { 402 | return window.self !== window.top; 403 | } 404 | 405 | _getUrlHash() { 406 | if (this._isEmbedded()) { 407 | return window.location.hash?.substring(1); 408 | } else { 409 | return window.top.location.hash?.substring(1); 410 | } 411 | } 412 | 413 | _getCurrentUrl() { 414 | if (this._isEmbedded()) { 415 | return window.location.href; 416 | } else { 417 | return window.top.location.href; 418 | } 419 | } 420 | 421 | _replaceUrlHash(value) { 422 | if (this._isEmbedded()) { 423 | return window.location.href.replace(window.location.hash, '#' + value); 424 | } else { 425 | return window.top.location.href.replace( 426 | window.top.location.hash, 427 | '#' + value 428 | ); 429 | } 430 | } 431 | 432 | async _copyToClipboard(textToCopy) { 433 | if (navigator.clipboard && window.isSecureContext && !this._isEmbedded()) { 434 | await navigator.clipboard.writeText(textToCopy); 435 | } else { 436 | const textArea = document.createElement('textarea'); 437 | textArea.value = textToCopy; 438 | textArea.style.position = 'absolute'; 439 | textArea.style.left = '-999999px'; 440 | document.body.prepend(textArea); 441 | textArea.select(); 442 | try { 443 | document.execCommand('copy'); 444 | } catch (error) { 445 | console.error(error); 446 | } finally { 447 | textArea.remove(); 448 | } 449 | } 450 | } 451 | } 452 | 453 | customElements.define('playground-stage', Playground); 454 | -------------------------------------------------------------------------------- /web/src/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | "use strict"; 6 | 7 | (() => { 8 | const enosys = () => { 9 | const err = new Error("not implemented"); 10 | err.code = "ENOSYS"; 11 | return err; 12 | }; 13 | 14 | if (!globalThis.fs) { 15 | let outputBuf = ""; 16 | globalThis.fs = { 17 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused 18 | writeSync(fd, buf) { 19 | outputBuf += decoder.decode(buf); 20 | const nl = outputBuf.lastIndexOf("\n"); 21 | if (nl != -1) { 22 | console.log(outputBuf.substring(0, nl)); 23 | outputBuf = outputBuf.substring(nl + 1); 24 | } 25 | return buf.length; 26 | }, 27 | write(fd, buf, offset, length, position, callback) { 28 | if (offset !== 0 || length !== buf.length || position !== null) { 29 | callback(enosys()); 30 | return; 31 | } 32 | const n = this.writeSync(fd, buf); 33 | callback(null, n); 34 | }, 35 | chmod(path, mode, callback) { callback(enosys()); }, 36 | chown(path, uid, gid, callback) { callback(enosys()); }, 37 | close(fd, callback) { callback(enosys()); }, 38 | fchmod(fd, mode, callback) { callback(enosys()); }, 39 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 40 | fstat(fd, callback) { callback(enosys()); }, 41 | fsync(fd, callback) { callback(null); }, 42 | ftruncate(fd, length, callback) { callback(enosys()); }, 43 | lchown(path, uid, gid, callback) { callback(enosys()); }, 44 | link(path, link, callback) { callback(enosys()); }, 45 | lstat(path, callback) { callback(enosys()); }, 46 | mkdir(path, perm, callback) { callback(enosys()); }, 47 | open(path, flags, mode, callback) { callback(enosys()); }, 48 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 49 | readdir(path, callback) { callback(enosys()); }, 50 | readlink(path, callback) { callback(enosys()); }, 51 | rename(from, to, callback) { callback(enosys()); }, 52 | rmdir(path, callback) { callback(enosys()); }, 53 | stat(path, callback) { callback(enosys()); }, 54 | symlink(path, link, callback) { callback(enosys()); }, 55 | truncate(path, length, callback) { callback(enosys()); }, 56 | unlink(path, callback) { callback(enosys()); }, 57 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 58 | }; 59 | } 60 | 61 | if (!globalThis.process) { 62 | globalThis.process = { 63 | getuid() { return -1; }, 64 | getgid() { return -1; }, 65 | geteuid() { return -1; }, 66 | getegid() { return -1; }, 67 | getgroups() { throw enosys(); }, 68 | pid: -1, 69 | ppid: -1, 70 | umask() { throw enosys(); }, 71 | cwd() { throw enosys(); }, 72 | chdir() { throw enosys(); }, 73 | } 74 | } 75 | 76 | if (!globalThis.path) { 77 | globalThis.path = { 78 | resolve(...pathSegments) { 79 | return pathSegments.join("/"); 80 | } 81 | } 82 | } 83 | 84 | if (!globalThis.crypto) { 85 | throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); 86 | } 87 | 88 | if (!globalThis.performance) { 89 | throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); 90 | } 91 | 92 | if (!globalThis.TextEncoder) { 93 | throw new Error("globalThis.TextEncoder is not available, polyfill required"); 94 | } 95 | 96 | if (!globalThis.TextDecoder) { 97 | throw new Error("globalThis.TextDecoder is not available, polyfill required"); 98 | } 99 | 100 | const encoder = new TextEncoder("utf-8"); 101 | const decoder = new TextDecoder("utf-8"); 102 | 103 | globalThis.Go = class { 104 | constructor() { 105 | this.argv = ["js"]; 106 | this.env = {}; 107 | this.exit = (code) => { 108 | if (code !== 0) { 109 | console.warn("exit code:", code); 110 | } 111 | }; 112 | this._exitPromise = new Promise((resolve) => { 113 | this._resolveExitPromise = resolve; 114 | }); 115 | this._pendingEvent = null; 116 | this._scheduledTimeouts = new Map(); 117 | this._nextCallbackTimeoutID = 1; 118 | 119 | const setInt64 = (addr, v) => { 120 | this.mem.setUint32(addr + 0, v, true); 121 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 122 | } 123 | 124 | const setInt32 = (addr, v) => { 125 | this.mem.setUint32(addr + 0, v, true); 126 | } 127 | 128 | const getInt64 = (addr) => { 129 | const low = this.mem.getUint32(addr + 0, true); 130 | const high = this.mem.getInt32(addr + 4, true); 131 | return low + high * 4294967296; 132 | } 133 | 134 | const loadValue = (addr) => { 135 | const f = this.mem.getFloat64(addr, true); 136 | if (f === 0) { 137 | return undefined; 138 | } 139 | if (!isNaN(f)) { 140 | return f; 141 | } 142 | 143 | const id = this.mem.getUint32(addr, true); 144 | return this._values[id]; 145 | } 146 | 147 | const storeValue = (addr, v) => { 148 | const nanHead = 0x7FF80000; 149 | 150 | if (typeof v === "number" && v !== 0) { 151 | if (isNaN(v)) { 152 | this.mem.setUint32(addr + 4, nanHead, true); 153 | this.mem.setUint32(addr, 0, true); 154 | return; 155 | } 156 | this.mem.setFloat64(addr, v, true); 157 | return; 158 | } 159 | 160 | if (v === undefined) { 161 | this.mem.setFloat64(addr, 0, true); 162 | return; 163 | } 164 | 165 | let id = this._ids.get(v); 166 | if (id === undefined) { 167 | id = this._idPool.pop(); 168 | if (id === undefined) { 169 | id = this._values.length; 170 | } 171 | this._values[id] = v; 172 | this._goRefCounts[id] = 0; 173 | this._ids.set(v, id); 174 | } 175 | this._goRefCounts[id]++; 176 | let typeFlag = 0; 177 | switch (typeof v) { 178 | case "object": 179 | if (v !== null) { 180 | typeFlag = 1; 181 | } 182 | break; 183 | case "string": 184 | typeFlag = 2; 185 | break; 186 | case "symbol": 187 | typeFlag = 3; 188 | break; 189 | case "function": 190 | typeFlag = 4; 191 | break; 192 | } 193 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 194 | this.mem.setUint32(addr, id, true); 195 | } 196 | 197 | const loadSlice = (addr) => { 198 | const array = getInt64(addr + 0); 199 | const len = getInt64(addr + 8); 200 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 201 | } 202 | 203 | const loadSliceOfValues = (addr) => { 204 | const array = getInt64(addr + 0); 205 | const len = getInt64(addr + 8); 206 | const a = new Array(len); 207 | for (let i = 0; i < len; i++) { 208 | a[i] = loadValue(array + i * 8); 209 | } 210 | return a; 211 | } 212 | 213 | const loadString = (addr) => { 214 | const saddr = getInt64(addr + 0); 215 | const len = getInt64(addr + 8); 216 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 217 | } 218 | 219 | const testCallExport = (a, b) => { 220 | this._inst.exports.testExport0(); 221 | return this._inst.exports.testExport(a, b); 222 | } 223 | 224 | const timeOrigin = Date.now() - performance.now(); 225 | this.importObject = { 226 | _gotest: { 227 | add: (a, b) => a + b, 228 | callExport: testCallExport, 229 | }, 230 | gojs: { 231 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 232 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 233 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 234 | // This changes the SP, thus we have to update the SP used by the imported function. 235 | 236 | // func wasmExit(code int32) 237 | "runtime.wasmExit": (sp) => { 238 | sp >>>= 0; 239 | const code = this.mem.getInt32(sp + 8, true); 240 | this.exited = true; 241 | delete this._inst; 242 | delete this._values; 243 | delete this._goRefCounts; 244 | delete this._ids; 245 | delete this._idPool; 246 | this.exit(code); 247 | }, 248 | 249 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 250 | "runtime.wasmWrite": (sp) => { 251 | sp >>>= 0; 252 | const fd = getInt64(sp + 8); 253 | const p = getInt64(sp + 16); 254 | const n = this.mem.getInt32(sp + 24, true); 255 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 256 | }, 257 | 258 | // func resetMemoryDataView() 259 | "runtime.resetMemoryDataView": (sp) => { 260 | sp >>>= 0; 261 | this.mem = new DataView(this._inst.exports.mem.buffer); 262 | }, 263 | 264 | // func nanotime1() int64 265 | "runtime.nanotime1": (sp) => { 266 | sp >>>= 0; 267 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 268 | }, 269 | 270 | // func walltime() (sec int64, nsec int32) 271 | "runtime.walltime": (sp) => { 272 | sp >>>= 0; 273 | const msec = (new Date).getTime(); 274 | setInt64(sp + 8, msec / 1000); 275 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 276 | }, 277 | 278 | // func scheduleTimeoutEvent(delay int64) int32 279 | "runtime.scheduleTimeoutEvent": (sp) => { 280 | sp >>>= 0; 281 | const id = this._nextCallbackTimeoutID; 282 | this._nextCallbackTimeoutID++; 283 | this._scheduledTimeouts.set(id, setTimeout( 284 | () => { 285 | this._resume(); 286 | while (this._scheduledTimeouts.has(id)) { 287 | // for some reason Go failed to register the timeout event, log and try again 288 | // (temporary workaround for https://github.com/golang/go/issues/28975) 289 | console.warn("scheduleTimeoutEvent: missed timeout event"); 290 | this._resume(); 291 | } 292 | }, 293 | getInt64(sp + 8), 294 | )); 295 | this.mem.setInt32(sp + 16, id, true); 296 | }, 297 | 298 | // func clearTimeoutEvent(id int32) 299 | "runtime.clearTimeoutEvent": (sp) => { 300 | sp >>>= 0; 301 | const id = this.mem.getInt32(sp + 8, true); 302 | clearTimeout(this._scheduledTimeouts.get(id)); 303 | this._scheduledTimeouts.delete(id); 304 | }, 305 | 306 | // func getRandomData(r []byte) 307 | "runtime.getRandomData": (sp) => { 308 | sp >>>= 0; 309 | crypto.getRandomValues(loadSlice(sp + 8)); 310 | }, 311 | 312 | // func finalizeRef(v ref) 313 | "syscall/js.finalizeRef": (sp) => { 314 | sp >>>= 0; 315 | const id = this.mem.getUint32(sp + 8, true); 316 | this._goRefCounts[id]--; 317 | if (this._goRefCounts[id] === 0) { 318 | const v = this._values[id]; 319 | this._values[id] = null; 320 | this._ids.delete(v); 321 | this._idPool.push(id); 322 | } 323 | }, 324 | 325 | // func stringVal(value string) ref 326 | "syscall/js.stringVal": (sp) => { 327 | sp >>>= 0; 328 | storeValue(sp + 24, loadString(sp + 8)); 329 | }, 330 | 331 | // func valueGet(v ref, p string) ref 332 | "syscall/js.valueGet": (sp) => { 333 | sp >>>= 0; 334 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 335 | sp = this._inst.exports.getsp() >>> 0; // see comment above 336 | storeValue(sp + 32, result); 337 | }, 338 | 339 | // func valueSet(v ref, p string, x ref) 340 | "syscall/js.valueSet": (sp) => { 341 | sp >>>= 0; 342 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 343 | }, 344 | 345 | // func valueDelete(v ref, p string) 346 | "syscall/js.valueDelete": (sp) => { 347 | sp >>>= 0; 348 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 349 | }, 350 | 351 | // func valueIndex(v ref, i int) ref 352 | "syscall/js.valueIndex": (sp) => { 353 | sp >>>= 0; 354 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 355 | }, 356 | 357 | // valueSetIndex(v ref, i int, x ref) 358 | "syscall/js.valueSetIndex": (sp) => { 359 | sp >>>= 0; 360 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 361 | }, 362 | 363 | // func valueCall(v ref, m string, args []ref) (ref, bool) 364 | "syscall/js.valueCall": (sp) => { 365 | sp >>>= 0; 366 | try { 367 | const v = loadValue(sp + 8); 368 | const m = Reflect.get(v, loadString(sp + 16)); 369 | const args = loadSliceOfValues(sp + 32); 370 | const result = Reflect.apply(m, v, args); 371 | sp = this._inst.exports.getsp() >>> 0; // see comment above 372 | storeValue(sp + 56, result); 373 | this.mem.setUint8(sp + 64, 1); 374 | } catch (err) { 375 | sp = this._inst.exports.getsp() >>> 0; // see comment above 376 | storeValue(sp + 56, err); 377 | this.mem.setUint8(sp + 64, 0); 378 | } 379 | }, 380 | 381 | // func valueInvoke(v ref, args []ref) (ref, bool) 382 | "syscall/js.valueInvoke": (sp) => { 383 | sp >>>= 0; 384 | try { 385 | const v = loadValue(sp + 8); 386 | const args = loadSliceOfValues(sp + 16); 387 | const result = Reflect.apply(v, undefined, args); 388 | sp = this._inst.exports.getsp() >>> 0; // see comment above 389 | storeValue(sp + 40, result); 390 | this.mem.setUint8(sp + 48, 1); 391 | } catch (err) { 392 | sp = this._inst.exports.getsp() >>> 0; // see comment above 393 | storeValue(sp + 40, err); 394 | this.mem.setUint8(sp + 48, 0); 395 | } 396 | }, 397 | 398 | // func valueNew(v ref, args []ref) (ref, bool) 399 | "syscall/js.valueNew": (sp) => { 400 | sp >>>= 0; 401 | try { 402 | const v = loadValue(sp + 8); 403 | const args = loadSliceOfValues(sp + 16); 404 | const result = Reflect.construct(v, args); 405 | sp = this._inst.exports.getsp() >>> 0; // see comment above 406 | storeValue(sp + 40, result); 407 | this.mem.setUint8(sp + 48, 1); 408 | } catch (err) { 409 | sp = this._inst.exports.getsp() >>> 0; // see comment above 410 | storeValue(sp + 40, err); 411 | this.mem.setUint8(sp + 48, 0); 412 | } 413 | }, 414 | 415 | // func valueLength(v ref) int 416 | "syscall/js.valueLength": (sp) => { 417 | sp >>>= 0; 418 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 419 | }, 420 | 421 | // valuePrepareString(v ref) (ref, int) 422 | "syscall/js.valuePrepareString": (sp) => { 423 | sp >>>= 0; 424 | const str = encoder.encode(String(loadValue(sp + 8))); 425 | storeValue(sp + 16, str); 426 | setInt64(sp + 24, str.length); 427 | }, 428 | 429 | // valueLoadString(v ref, b []byte) 430 | "syscall/js.valueLoadString": (sp) => { 431 | sp >>>= 0; 432 | const str = loadValue(sp + 8); 433 | loadSlice(sp + 16).set(str); 434 | }, 435 | 436 | // func valueInstanceOf(v ref, t ref) bool 437 | "syscall/js.valueInstanceOf": (sp) => { 438 | sp >>>= 0; 439 | this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); 440 | }, 441 | 442 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 443 | "syscall/js.copyBytesToGo": (sp) => { 444 | sp >>>= 0; 445 | const dst = loadSlice(sp + 8); 446 | const src = loadValue(sp + 32); 447 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 448 | this.mem.setUint8(sp + 48, 0); 449 | return; 450 | } 451 | const toCopy = src.subarray(0, dst.length); 452 | dst.set(toCopy); 453 | setInt64(sp + 40, toCopy.length); 454 | this.mem.setUint8(sp + 48, 1); 455 | }, 456 | 457 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 458 | "syscall/js.copyBytesToJS": (sp) => { 459 | sp >>>= 0; 460 | const dst = loadValue(sp + 8); 461 | const src = loadSlice(sp + 16); 462 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 463 | this.mem.setUint8(sp + 48, 0); 464 | return; 465 | } 466 | const toCopy = src.subarray(0, dst.length); 467 | dst.set(toCopy); 468 | setInt64(sp + 40, toCopy.length); 469 | this.mem.setUint8(sp + 48, 1); 470 | }, 471 | 472 | "debug": (value) => { 473 | console.log(value); 474 | }, 475 | } 476 | }; 477 | } 478 | 479 | async run(instance) { 480 | if (!(instance instanceof WebAssembly.Instance)) { 481 | throw new Error("Go.run: WebAssembly.Instance expected"); 482 | } 483 | this._inst = instance; 484 | this.mem = new DataView(this._inst.exports.mem.buffer); 485 | this._values = [ // JS values that Go currently has references to, indexed by reference id 486 | NaN, 487 | 0, 488 | null, 489 | true, 490 | false, 491 | globalThis, 492 | this, 493 | ]; 494 | this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id 495 | this._ids = new Map([ // mapping from JS values to reference ids 496 | [0, 1], 497 | [null, 2], 498 | [true, 3], 499 | [false, 4], 500 | [globalThis, 5], 501 | [this, 6], 502 | ]); 503 | this._idPool = []; // unused ids that have been garbage collected 504 | this.exited = false; // whether the Go program has exited 505 | 506 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 507 | let offset = 4096; 508 | 509 | const strPtr = (str) => { 510 | const ptr = offset; 511 | const bytes = encoder.encode(str + "\0"); 512 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 513 | offset += bytes.length; 514 | if (offset % 8 !== 0) { 515 | offset += 8 - (offset % 8); 516 | } 517 | return ptr; 518 | }; 519 | 520 | const argc = this.argv.length; 521 | 522 | const argvPtrs = []; 523 | this.argv.forEach((arg) => { 524 | argvPtrs.push(strPtr(arg)); 525 | }); 526 | argvPtrs.push(0); 527 | 528 | const keys = Object.keys(this.env).sort(); 529 | keys.forEach((key) => { 530 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 531 | }); 532 | argvPtrs.push(0); 533 | 534 | const argv = offset; 535 | argvPtrs.forEach((ptr) => { 536 | this.mem.setUint32(offset, ptr, true); 537 | this.mem.setUint32(offset + 4, 0, true); 538 | offset += 8; 539 | }); 540 | 541 | // The linker guarantees global data starts from at least wasmMinDataAddr. 542 | // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. 543 | const wasmMinDataAddr = 4096 + 8192; 544 | if (offset >= wasmMinDataAddr) { 545 | throw new Error("total length of command line and environment variables exceeds limit"); 546 | } 547 | 548 | this._inst.exports.run(argc, argv); 549 | if (this.exited) { 550 | this._resolveExitPromise(); 551 | } 552 | await this._exitPromise; 553 | } 554 | 555 | _resume() { 556 | if (this.exited) { 557 | throw new Error("Go program has already exited"); 558 | } 559 | this._inst.exports.resume(); 560 | if (this.exited) { 561 | this._resolveExitPromise(); 562 | } 563 | } 564 | 565 | _makeFuncWrapper(id) { 566 | const go = this; 567 | return function () { 568 | const event = { id: id, this: this, args: arguments }; 569 | go._pendingEvent = event; 570 | go._resume(); 571 | return event.result; 572 | }; 573 | } 574 | } 575 | })(); 576 | --------------------------------------------------------------------------------