├── .github ├── CODEOWNERS ├── pull_request_template.md └── workflows │ ├── docker.yml │ ├── s3.yml │ ├── test.yml │ ├── unstable.yml │ └── update-license-year.yml ├── .gitignore ├── CHANGES.txt ├── CONTRIBUTORS-GUIDE.md ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── proxy │ └── main.go └── synchronizer │ └── main.go ├── docker ├── Dockerfile.proxy ├── Dockerfile.synchronizer ├── entrypoint.sh.tpl ├── functions.sh └── util │ └── clilist │ └── main.go ├── go.mod ├── go.sum ├── release ├── commitversion.go.template ├── dp_gen.py ├── install_script_template ├── versions.css.tpl ├── versions.download-row.html.tpl ├── versions.html.tpl ├── versions.pos.html.tpl └── versions.pre.html.tpl ├── sonar-project.properties ├── splitio ├── admin │ ├── admin.go │ ├── common │ │ └── config.go │ ├── controllers │ │ ├── dashboard.go │ │ ├── healthcheck.go │ │ ├── healthcheck_test.go │ │ ├── helpers.go │ │ ├── info.go │ │ ├── observability.go │ │ ├── observability_test.go │ │ ├── shutdown.go │ │ ├── snapshot.go │ │ └── snapshot_test.go │ └── views │ │ └── dashboard │ │ ├── bootstrap.go │ │ ├── chartjs.go │ │ ├── datainspector.go │ │ ├── js.go │ │ ├── logo.go │ │ ├── main.go │ │ ├── menu.go │ │ ├── queuemanager.go │ │ └── stats.go ├── banner.go ├── commitversion.go ├── common │ ├── conf │ │ ├── advanced.go │ │ ├── basic.go │ │ ├── file.go │ │ ├── parser.go │ │ ├── parser_test.go │ │ ├── sections.go │ │ ├── validators.go │ │ └── validators_test.go │ ├── errors.go │ ├── impressionlistener │ │ ├── dtos.go │ │ ├── listener.go │ │ ├── listener_test.go │ │ └── mocks │ │ │ └── listener.go │ ├── runtime.go │ ├── snapshot │ │ ├── snapshot.go │ │ └── snapshot_test.go │ ├── storage │ │ └── snapshotter.go │ └── sync │ │ └── sync.go ├── enforce_fips.go ├── log │ ├── custom.go │ ├── custom_test.go │ ├── initialization.go │ └── slack.go ├── producer │ ├── conf │ │ └── sections.go │ ├── evcalc │ │ ├── evcalc.go │ │ ├── evcalc_test.go │ │ └── mocks │ │ │ └── evcalc.go │ ├── initialization.go │ ├── initialization_test.go │ ├── storage │ │ ├── mocks │ │ │ └── telemetry.go │ │ ├── telemetry.go │ │ └── telemetry_test.go │ ├── task │ │ ├── events.go │ │ ├── events_test.go │ │ ├── impcounts.go │ │ ├── impressions.go │ │ ├── impressions_test.go │ │ ├── pipelined.go │ │ ├── pipelined_test.go │ │ ├── telemetry.go │ │ ├── uniquekeys.go │ │ └── uniquekeys_test.go │ ├── util.go │ └── worker │ │ ├── impcounts.go │ │ ├── telemetry.go │ │ └── telemetry_test.go ├── provisional │ ├── healthcheck │ │ ├── application │ │ │ ├── counter │ │ │ │ ├── basecounter.go │ │ │ │ ├── periodic.go │ │ │ │ ├── periodic_test.go │ │ │ │ ├── threshold.go │ │ │ │ └── threshold_test.go │ │ │ ├── monitor.go │ │ │ └── monitor_test.go │ │ └── services │ │ │ ├── counter │ │ │ ├── bypercentage.go │ │ │ └── bypercentage_test.go │ │ │ ├── monitor.go │ │ │ └── monitor_test.go │ └── observability │ │ ├── segment_wrapper.go │ │ ├── segment_wrapper_test.go │ │ ├── split_wrapper.go │ │ └── split_wrapper_test.go ├── proxy │ ├── caching │ │ ├── caching.go │ │ ├── caching_test.go │ │ ├── mocks │ │ │ └── mock.go │ │ ├── workers.go │ │ └── workers_test.go │ ├── conf │ │ └── sections.go │ ├── controllers │ │ ├── auth.go │ │ ├── events.go │ │ ├── events_test.go │ │ ├── middleware │ │ │ ├── auth.go │ │ │ ├── auth_test.go │ │ │ ├── endpoint.go │ │ │ ├── latency.go │ │ │ └── latency_test.go │ │ ├── sdk.go │ │ ├── sdk_test.go │ │ ├── telemetry.go │ │ ├── telemetry_test.go │ │ └── util.go │ ├── doc.go │ ├── flagsets │ │ ├── flagsets.go │ │ └── flagsets_test.go │ ├── initialization.go │ ├── initialization_test.go │ ├── internal │ │ └── dtos.go │ ├── proxy.go │ ├── proxy_test.go │ ├── storage │ │ ├── mocks │ │ │ └── mocks.go │ │ ├── optimized │ │ │ ├── historic.go │ │ │ ├── historic_test.go │ │ │ ├── mocks │ │ │ │ └── mocks.go │ │ │ ├── mysegments.go │ │ │ └── mysegments_test.go │ │ ├── persistent │ │ │ ├── boltdb.go │ │ │ ├── helpers.go │ │ │ ├── helpers_test.go │ │ │ ├── mocks │ │ │ │ └── segment.go │ │ │ ├── segments.go │ │ │ ├── segments_test.go │ │ │ ├── splits.go │ │ │ └── splits_test.go │ │ ├── segments.go │ │ ├── segments_test.go │ │ ├── splits.go │ │ ├── splits_test.go │ │ ├── telemetry.go │ │ ├── telemetryts.go │ │ └── telemetryts_test.go │ └── tasks │ │ ├── deferred.go │ │ ├── events.go │ │ ├── impcount.go │ │ ├── impressions.go │ │ ├── mocks │ │ └── deferred.go │ │ └── telemetry.go ├── util │ ├── tls.go │ ├── tls_test.go │ ├── utils.go │ └── utils_test.go └── version.go ├── test ├── certs │ ├── client-cert.pem │ ├── client-key.pem │ ├── https │ │ ├── Makefile │ │ ├── admin.crt │ │ ├── admin.key │ │ ├── ca.crt │ │ ├── ca.key │ │ ├── openssl.conf │ │ ├── proxy.crt │ │ └── proxy.key │ └── root-cert.pem ├── dataset │ ├── test.conf.error1.json │ ├── test.conf.error2.json │ ├── test.conf.error3.json │ ├── test.conf.error4.json │ ├── test.conf.error5.json │ └── test.conf.json ├── murmur │ ├── murmur3-sample-data-non-alpha-numeric-v2.csv │ └── murmur3-sample-data-v2.csv └── snapshot │ └── proxy.snapshot └── windows ├── Makefile ├── build_from_mac.sh ├── entrypoint.sh └── macos_builder.Dockerfile /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @splitio/sdk 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Split Synchronizer 2 | 3 | ## What did you accomplish? 4 | 5 | ## How do we test the changes introduced in this PR? 6 | 7 | ## Extra Notes 8 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.run_number || github.event.pull_request.number }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | docker: 17 | name: Build Docker image 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | app: 22 | - synchronizer 23 | - proxy 24 | fips_mode: 25 | - enabled 26 | - disabled 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | 31 | - name: Setup QEMU 32 | uses: docker/setup-qemu-action@v3 33 | with: 34 | platforms: amd64,arm64 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | - name: Login to Dockerhub 40 | uses: docker/login-action@v3 41 | with: 42 | username: ${{ vars.DOCKERHUB_USERNAME }} 43 | password: ${{ secrets.DOCKERHUB_RO_TOKEN }} 44 | 45 | - name: Login to Artifactory 46 | if: ${{ github.event_name == 'push' }} 47 | uses: docker/login-action@v3 48 | with: 49 | registry: ${{ vars.ARTIFACTORY_DOCKER_REGISTRY }} 50 | username: ${{ vars.ARTIFACTORY_DOCKER_USER }} 51 | password: ${{ secrets.ARTIFACTORY_DOCKER_PASS }} 52 | 53 | - name: Get version 54 | run: echo "VERSION=$(awk '/^const Version/{gsub(/"/, "", $4); print $4}' splitio/version.go)" >> $GITHUB_ENV 55 | 56 | - name: Docker Build and Push 57 | uses: docker/build-push-action@v6 58 | with: 59 | context: . 60 | file: docker/Dockerfile.${{ matrix.app }} 61 | push: ${{ github.event_name == 'push' }} 62 | platforms: linux/amd64,linux/arm64 63 | tags: ${{ vars.ARTIFACTORY_DOCKER_REGISTRY }}/split-${{ matrix.app }}${{ matrix.fips_mode == 'enabled' && '-fips' || ''}}:${{ env.VERSION }},${{ vars.ARTIFACTORY_DOCKER_REGISTRY }}/split-${{ matrix.app }}${{ matrix.fips_mode == 'enabled' && '-fips' || '' }}:latest 64 | build-args: | 65 | FIPS_MODE=${{ matrix.fips_mode }} 66 | 67 | lacework: 68 | name: Scan Docker image 69 | if: ${{ github.event_name == 'pull_request' }} 70 | runs-on: ubuntu-latest 71 | strategy: 72 | matrix: 73 | app: 74 | - synchronizer 75 | - proxy 76 | fips_mode: 77 | - enabled 78 | - disabled 79 | steps: 80 | - name: Checkout code 81 | uses: actions/checkout@v4 82 | 83 | - name: Get version 84 | run: echo "VERSION=$(awk '/^const Version/{gsub(/"/, "", $4); print $4}' splitio/version.go)" >> $GITHUB_ENV 85 | 86 | - name: Login to Dockerhub 87 | uses: docker/login-action@v3 88 | with: 89 | username: ${{ vars.DOCKERHUB_USERNAME }} 90 | password: ${{ secrets.DOCKERHUB_RO_TOKEN }} 91 | 92 | - name: Docker Build and Push 93 | uses: docker/build-push-action@v6 94 | with: 95 | context: . 96 | file: docker/Dockerfile.${{ matrix.app }} 97 | push: false 98 | tags: ${{ vars.ARTIFACTORY_DOCKER_REGISTRY }}/split-${{ matrix.app }}${{ matrix.fips_mode == 'enabled' && '-fips' || ''}}:${{ env.VERSION }} 99 | build-args: | 100 | FIPS_MODE=${{ matrix.fips_mode }} 101 | 102 | - name: Scan container using Lacework 103 | uses: lacework/lw-scanner-action@v1.4.3 104 | with: 105 | LW_ACCOUNT_NAME: ${{ vars.LW_ACCOUNT_NAME }} 106 | LW_ACCESS_TOKEN: ${{ secrets.LW_ACCESS_TOKEN }} 107 | IMAGE_NAME: ${{ vars.ARTIFACTORY_DOCKER_REGISTRY }}/split-${{ matrix.app }}${{ matrix.fips_mode == 'enabled' && '-fips' || ''}} 108 | IMAGE_TAG: ${{ env.VERSION }} 109 | SAVE_RESULTS_IN_LACEWORK: true 110 | RESULTS_IN_GITHUB_SUMMARY: true 111 | -------------------------------------------------------------------------------- /.github/workflows/s3.yml: -------------------------------------------------------------------------------- 1 | name: cd 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | jobs: 16 | build-publish: 17 | name: Build and publish to S3 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Setup Go 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version: '1.23.3' 29 | 30 | - name: Create build folder 31 | run: mkdir -p build 32 | 33 | - name: Execute build 34 | run: make release_assets 35 | 36 | - name: Configure AWS credentials 37 | if: ${{ github.event_name == 'push' }} 38 | uses: aws-actions/configure-aws-credentials@v4 39 | with: 40 | role-to-assume: arn:aws:iam::825951051969:role/gha-downloads-role 41 | aws-region: us-east-1 42 | 43 | - name: Deploy to S3 44 | if: ${{ github.event_name == 'push' }} 45 | run: aws s3 sync $SOURCE_DIR s3://$BUCKET 46 | env: 47 | BUCKET: downloads.split.io 48 | SOURCE_DIR: ./build 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches-ignore: 9 | - none 10 | 11 | jobs: 12 | build-and-test: 13 | name: Build and run tests 14 | runs-on: ubuntu-latest 15 | services: 16 | redis: 17 | image: redis 18 | credentials: 19 | username: ${{ vars.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_RO_TOKEN }} 21 | ports: 22 | - 6379:6379 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Setup Go 30 | uses: actions/setup-go@v5 31 | with: 32 | go-version: '1.23.3' 33 | 34 | - name: Get version 35 | run: echo "VERSION=$(awk '/^const Version/{gsub(/"/, "", $4); print $4}' splitio/version.go)" >> $GITHUB_ENV 36 | 37 | - name: Run test 38 | run: make test_coverage 39 | 40 | - name: SonarQube Scan (Pull Request) 41 | if: ${{ github.event_name == 'pull_request' }} 42 | uses: SonarSource/sonarcloud-github-action@v3 43 | env: 44 | SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} 45 | with: 46 | projectBaseDir: . 47 | args: > 48 | -Dsonar.host.url=${{ secrets.SONARQUBE_HOST }} 49 | -Dsonar.projectVersion=${{ env.VERSION }} 50 | -Dsonar.pullrequest.key=${{ github.event.pull_request.number }} 51 | -Dsonar.pullrequest.branch=${{ github.event.pull_request.head.ref }} 52 | -Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }} 53 | 54 | - name: SonarQube Scan (Push) 55 | if: ${{ github.event_name == 'push' }} 56 | uses: SonarSource/sonarcloud-github-action@v3 57 | env: 58 | SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} 59 | with: 60 | projectBaseDir: . 61 | args: > 62 | -Dsonar.host.url=${{ vars.SONARQUBE_HOST }} 63 | -Dsonar.projectVersion=${{ env.VERSION }} 64 | -------------------------------------------------------------------------------- /.github/workflows/unstable.yml: -------------------------------------------------------------------------------- 1 | name: unstable 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - master 7 | 8 | jobs: 9 | push-docker-image: 10 | name: Build and Push Docker Image 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | app: 15 | - synchronizer 16 | - proxy 17 | fips_mode: 18 | - enabled 19 | - disabled 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup QEMU 25 | uses: docker/setup-qemu-action@v3 26 | with: 27 | platforms: amd64,arm64 28 | 29 | - name: Set up Docker Buildx 30 | uses: docker/setup-buildx-action@v3 31 | 32 | - name: Login to Dockerhub 33 | uses: docker/login-action@v3 34 | with: 35 | username: ${{ vars.DOCKERHUB_USERNAME }} 36 | password: ${{ secrets.DOCKERHUB_RO_TOKEN }} 37 | 38 | - name: Login to Artifactory 39 | uses: docker/login-action@v3 40 | with: 41 | registry: splitio-docker-dev.jfrog.io 42 | username: ${{ vars.ARTIFACTORY_DOCKER_USER }} 43 | password: ${{ secrets.ARTIFACTORY_DOCKER_PASS }} 44 | 45 | - name: Get short hash 46 | run: echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 47 | 48 | - name: Docker Build and Push 49 | uses: docker/build-push-action@v6 50 | with: 51 | context: . 52 | file: docker/Dockerfile.${{ matrix.app }} 53 | push: true 54 | platforms: linux/amd64,linux/arm64 55 | tags: splitio-docker-dev.jfrog.io/split-${{ matrix.app }}${{ matrix.fips_mode == 'enabled' && '-fips' || '' }}:${{ env.SHORT_SHA }} 56 | build-args: | 57 | FIPS_MODE=${{ matrix.fips_mode }} 58 | -------------------------------------------------------------------------------- /.github/workflows/update-license-year.yml: -------------------------------------------------------------------------------- 1 | name: Update License Year 2 | 3 | on: 4 | schedule: 5 | - cron: "0 3 1 1 *" # 03:00 AM on January 1 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set Current year 21 | run: "echo CURRENT=$(date +%Y) >> $GITHUB_ENV" 22 | 23 | - name: Set Previous Year 24 | run: "echo PREVIOUS=$(($CURRENT-1)) >> $GITHUB_ENV" 25 | 26 | - name: Update LICENSE 27 | uses: jacobtomlinson/gha-find-replace@v2 28 | with: 29 | find: ${{ env.PREVIOUS }} 30 | replace: ${{ env.CURRENT }} 31 | include: "LICENSE" 32 | regex: false 33 | 34 | - name: Commit files 35 | run: | 36 | git config user.name 'github-actions[bot]' 37 | git config user.email 'github-actions[bot]@users.noreply.github.com' 38 | git commit -m "Updated License Year" -a 39 | 40 | - name: Create Pull Request 41 | uses: peter-evans/create-pull-request@v3 42 | with: 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | title: Update License Year 45 | branch: update-license 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Project files 2 | split-sync 3 | split-proxy 4 | split-sync-fips 5 | split-proxy-fips 6 | proxy-opts.md 7 | sync-opts.md 8 | 9 | coverage.out 10 | entrypoint.proxy.sh 11 | entrypoint.sync.sh 12 | entrypoint.synchronizer.sh 13 | run.sh 14 | bin/* 15 | *.yaml 16 | /vendor 17 | 18 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 19 | *.o 20 | *.a 21 | *.so 22 | 23 | # Folders 24 | _obj 25 | _test 26 | 27 | # Architecture specific extensions/prefixes 28 | *.[568vq] 29 | [568vq].out 30 | 31 | *.cgo1.go 32 | *.cgo2.c 33 | _cgo_defun.c 34 | _cgo_gotypes.go 35 | _cgo_export.* 36 | 37 | _testmain.go 38 | 39 | *.exe 40 | *.test 41 | *.prof 42 | 43 | .DS_Store 44 | *.json 45 | !/test/dataset/test.conf.json 46 | !/test/dataset/test.conf.error1.json 47 | !/test/dataset/test.conf.error2.json 48 | !/test/dataset/test.conf.error3.json 49 | !/test/dataset/test.conf.error4.json 50 | !/test/dataset/test.conf.error5.json 51 | !/test/dataset/test.conf.json 52 | !/test/dataset/test.conf.warning1.json 53 | !/test/dataset/test.conf.warning2.json 54 | !/test/dataset/test.conf.warning3.json 55 | !/test/dataset/test.conf.warning4.json 56 | !/test/dataset/test.conf.warning5.json 57 | 58 | # vim backup files 59 | *.swp 60 | 61 | # Release files 62 | release/*.bin 63 | release/*.zip 64 | release/versions.html 65 | 66 | Gopkg.lock 67 | 68 | .vscode/* 69 | 70 | build/* 71 | split-proxy 72 | split-sync 73 | /clilist 74 | 75 | windows/downloads 76 | windows/unpacked 77 | windows/build 78 | -------------------------------------------------------------------------------- /CONTRIBUTORS-GUIDE.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Split Synchronizer 2 | 3 | Split Synchronizer is an open source project and we welcome feedback and contribution. The information below describes how to build the project with your changes, run the tests, and send the Pull Request(PR). 4 | 5 | ## Development 6 | 7 | ### Development process 8 | 9 | 1. Fork the repository and create a topic branch from `development` branch. Please use a descriptive name for your branch. 10 | 2. While developing, use descriptive messages in your commits. Avoid short or meaningless sentences like "fix bug". 11 | 3. Make sure to add tests for both positive and negative cases. 12 | 4. Run the linter script of the project and fix any issues you find. 13 | 5. Run the build script and make sure it runs with no errors. 14 | 6. Run all tests and make sure there are no failures. 15 | 7. `git push` your changes to GitHub within your topic branch. 16 | 8. Open a Pull Request(PR) from your forked repo and into the `development` branch of the original repository. 17 | 9. When creating your PR, please fill out all the fields of the PR template, as applicable, for the project. 18 | 10. Check for conflicts once the pull request is created to make sure your PR can be merged cleanly into `development`. 19 | 11. Keep an eye out for any feedback or comments from Split's SDK team. 20 | 21 | ### Building the SDK 22 | 23 | #### Usage with go 24 | If you're just trying to run the app, run `dep ensure`` on the root of the project. 25 | Then, execute `go run main.go` 26 | 27 | #### Docker 28 | If you want to build a Docker Image, you need to execute the following command at root folder: docker build -t splitsoftware/split-synchronizer:X.X.X . 29 | 30 | ### Running tests 31 | 32 | You can use `go test ./..` in root directory for running the tests. 33 | 34 | # Contact 35 | 36 | If you have any other questions or need to contact us directly in a private manner send us a note at sdks@split.io. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2025 Split Software, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /cmd/proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/splitio/split-synchronizer/v5/splitio" 9 | "github.com/splitio/split-synchronizer/v5/splitio/common" 10 | cconf "github.com/splitio/split-synchronizer/v5/splitio/common/conf" 11 | "github.com/splitio/split-synchronizer/v5/splitio/log" 12 | "github.com/splitio/split-synchronizer/v5/splitio/proxy" 13 | "github.com/splitio/split-synchronizer/v5/splitio/proxy/conf" 14 | ) 15 | 16 | const ( 17 | exitCodeSuccess = 0 18 | exitCodeConfigError = 1 19 | ) 20 | 21 | func parseCliArgs() *cconf.CliFlags { 22 | return cconf.ParseCliArgs(&conf.Main{}) 23 | } 24 | 25 | func setupConfig(cliArgs *cconf.CliFlags) (*conf.Main, error) { 26 | proxyConf := conf.Main{} 27 | cconf.PopulateDefaults(&proxyConf) 28 | 29 | if path := *cliArgs.ConfigFile; path != "" { 30 | err := cconf.PopulateConfigFromFile(path, &proxyConf) 31 | if err != nil { 32 | return nil, fmt.Errorf("error parsing config file: %w", err) 33 | } 34 | } 35 | 36 | cconf.PopulateFromArguments(&proxyConf, cliArgs.RawConfig) 37 | 38 | var err error 39 | proxyConf.FlagSetsFilter, err = cconf.ValidateFlagsets(proxyConf.FlagSetsFilter) 40 | return &proxyConf, err 41 | } 42 | 43 | func main() { 44 | fmt.Println(splitio.ASCILogo) 45 | fmt.Printf("\nSplit Proxy - Version: %s (%s) \n", splitio.Version, splitio.CommitVersion) 46 | 47 | cliArgs := parseCliArgs() 48 | if *cliArgs.VersionInfo { //already printed, we can now exit 49 | os.Exit(exitCodeSuccess) 50 | } 51 | 52 | if fn := *cliArgs.WriteDefaultConfigFile; fn != "" { 53 | if err := cconf.WriteDefaultConfigFile(fn, &conf.Main{}); err != nil { 54 | fmt.Printf("error writing config file with default values: %s", err.Error()) 55 | os.Exit(exitCodeConfigError) 56 | } 57 | fmt.Println("Configuration file written successfully to: ", fn) 58 | os.Exit(exitCodeSuccess) 59 | } 60 | 61 | cfg, err := setupConfig(cliArgs) 62 | if err != nil { 63 | var fsErr cconf.FlagSetValidationError 64 | if errors.As(err, &fsErr) { 65 | fmt.Println("error processing flagsets: ", err.Error()) 66 | } else { 67 | fmt.Println("error processing config: ", err) 68 | os.Exit(exitCodeConfigError) 69 | } 70 | } 71 | 72 | logger := log.BuildFromConfig(&cfg.Logging, "Split-Proxy", &cfg.Integrations.Slack) 73 | err = proxy.Start(logger, cfg) 74 | 75 | if err == nil { 76 | return 77 | } 78 | 79 | var initError *common.InitializationError 80 | if errors.As(err, &initError) { 81 | logger.Error("Failed to initialize the split sync: ", initError) 82 | os.Exit(initError.ExitCode()) 83 | } 84 | 85 | os.Exit(common.ExitUndefined) 86 | } 87 | -------------------------------------------------------------------------------- /cmd/synchronizer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/splitio/split-synchronizer/v5/splitio" 9 | "github.com/splitio/split-synchronizer/v5/splitio/common" 10 | cconf "github.com/splitio/split-synchronizer/v5/splitio/common/conf" 11 | "github.com/splitio/split-synchronizer/v5/splitio/log" 12 | "github.com/splitio/split-synchronizer/v5/splitio/producer" 13 | "github.com/splitio/split-synchronizer/v5/splitio/producer/conf" 14 | ) 15 | 16 | const ( 17 | exitCodeSuccess = 0 18 | exitCodeConfigError = 1 19 | ) 20 | 21 | func parseCliArgs() *cconf.CliFlags { 22 | return cconf.ParseCliArgs(&conf.Main{}) 23 | } 24 | 25 | func setupConfig(cliArgs *cconf.CliFlags) (*conf.Main, error) { 26 | syncConf := conf.Main{} 27 | cconf.PopulateDefaults(&syncConf) 28 | 29 | if path := *cliArgs.ConfigFile; path != "" { 30 | err := cconf.PopulateConfigFromFile(path, &syncConf) 31 | if err != nil { 32 | return nil, fmt.Errorf("error parsing config file: %w", err) 33 | } 34 | } 35 | 36 | cconf.PopulateFromArguments(&syncConf, cliArgs.RawConfig) 37 | 38 | var err error 39 | syncConf.FlagSetsFilter, err = cconf.ValidateFlagsets(syncConf.FlagSetsFilter) 40 | return &syncConf, err 41 | } 42 | 43 | func main() { 44 | fmt.Println(splitio.ASCILogo) 45 | fmt.Printf("\nSplit Synchronizer - Version: %s (%s) \n", splitio.Version, splitio.CommitVersion) 46 | 47 | cliArgs := parseCliArgs() 48 | if *cliArgs.VersionInfo { 49 | os.Exit(exitCodeSuccess) 50 | } 51 | 52 | if fn := *cliArgs.WriteDefaultConfigFile; fn != "" { 53 | if err := cconf.WriteDefaultConfigFile(fn, &conf.Main{}); err != nil { 54 | fmt.Printf("error writing config file with default values: %s", err.Error()) 55 | os.Exit(exitCodeConfigError) 56 | } 57 | fmt.Println("Configuration file written successfully to: ", fn) 58 | os.Exit(exitCodeSuccess) 59 | } 60 | 61 | cfg, err := setupConfig(cliArgs) 62 | if err != nil { 63 | var fsErr cconf.FlagSetValidationError 64 | if errors.As(err, &fsErr) { 65 | fmt.Println("error processing flagset: ", err.Error()) 66 | } else { 67 | fmt.Println("error processing config: ", err) 68 | os.Exit(exitCodeConfigError) 69 | } 70 | } 71 | 72 | logger := log.BuildFromConfig(&cfg.Logging, "Split-Sync", &cfg.Integrations.Slack) 73 | err = producer.Start(logger, cfg) 74 | 75 | if err == nil { 76 | return 77 | } 78 | 79 | var initError *common.InitializationError 80 | if errors.As(err, &initError) { 81 | logger.Error("Failed to initialize the split sync: ", initError) 82 | os.Exit(initError.ExitCode()) 83 | } 84 | 85 | os.Exit(common.ExitUndefined) 86 | } 87 | -------------------------------------------------------------------------------- /docker/Dockerfile.proxy: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM golang:1.23.9-bookworm AS builder 3 | 4 | ARG EXTRA_BUILD_ARGS 5 | ARG FIPS_MODE 6 | 7 | RUN apt update -y 8 | RUN apt install -y build-essential ca-certificates python3 git 9 | 10 | WORKDIR /code 11 | 12 | COPY . . 13 | 14 | RUN bash -c 'if [[ "${FIPS_MODE}" = "enabled" ]]; \ 15 | then echo "building in fips mode"; make clean split-proxy-fips entrypoints EXTRA_BUILD_ARGS="${EXTRA_BUILD_ARGS}"; mv split-proxy-fips split-proxy; \ 16 | else echo "building in standard mode"; make clean split-proxy entrypoints EXTRA_BUILD_ARGS="${EXTRA_BUILD_ARGS}"; \ 17 | fi' 18 | 19 | # Runner stage 20 | FROM debian:12.11 AS runner 21 | 22 | RUN apt update -y 23 | RUN apt install -y bash ca-certificates 24 | RUN addgroup --gid 1000 --system 'split-proxy' 25 | RUN adduser \ 26 | --disabled-password \ 27 | --gecos '' \ 28 | --ingroup 'split-proxy' \ 29 | --no-create-home \ 30 | --system \ 31 | --uid 1000 \ 32 | 'split-proxy' 33 | 34 | COPY docker/functions.sh . 35 | 36 | COPY --from=builder /code/split-proxy /usr/bin/ 37 | COPY --from=builder /code/entrypoint.proxy.sh . 38 | 39 | EXPOSE 3000 3010 40 | 41 | USER 'split-proxy' 42 | 43 | ENTRYPOINT ["bash", "entrypoint.proxy.sh"] 44 | -------------------------------------------------------------------------------- /docker/Dockerfile.synchronizer: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM golang:1.23.9-bookworm AS builder 3 | 4 | ARG EXTRA_BUILD_ARGS 5 | ARG FIPS_MODE 6 | 7 | RUN apt update -y 8 | RUN apt install -y build-essential ca-certificates python3 git 9 | 10 | WORKDIR /code 11 | 12 | COPY . . 13 | 14 | RUN bash -c 'if [[ "${FIPS_MODE}" = "enabled" ]]; \ 15 | then echo "building in fips mode"; make clean split-sync-fips entrypoints EXTRA_BUILD_ARGS="${EXTRA_BUILD_ARGS}"; mv split-sync-fips split-sync; \ 16 | else echo "building in standard mode"; make clean split-sync entrypoints EXTRA_BUILD_ARGS="${EXTRA_BUILD_ARGS}"; \ 17 | fi' 18 | 19 | # Runner stage 20 | FROM debian:12.11 AS runner 21 | 22 | RUN apt update -y 23 | RUN apt install -y bash ca-certificates 24 | RUN addgroup --gid 1000 --system 'split-synchronizer' 25 | RUN adduser \ 26 | --disabled-password \ 27 | --gecos '' \ 28 | --ingroup 'split-synchronizer' \ 29 | --no-create-home \ 30 | --system \ 31 | --uid 1000 \ 32 | 'split-synchronizer' 33 | 34 | COPY docker/functions.sh . 35 | 36 | COPY --from=builder /code/split-sync /usr/bin/ 37 | COPY --from=builder /code/entrypoint.synchronizer.sh . 38 | 39 | EXPOSE 3000 3010 40 | 41 | USER 'split-synchronizer' 42 | 43 | ENTRYPOINT ["bash", "entrypoint.synchronizer.sh"] 44 | -------------------------------------------------------------------------------- /docker/entrypoint.sh.tpl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | FLAGS=({{ARGS}}) 4 | 5 | source functions.sh 6 | cli_args=$(parse_env {{PREFIX}} "${FLAGS[@]}") 7 | exec {{EXECUTABLE}} $cli_args 8 | -------------------------------------------------------------------------------- /docker/functions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function parse_flags_from_conf_file() { 4 | fn=$1 5 | 6 | regex=".*s-cli:\"([^ ]+)\" .*" 7 | while IFS="" read -r line || [ -n "$line" ]; do 8 | if [[ $line =~ $regex ]]; then 9 | name="${BASH_REMATCH[1]}" 10 | echo $name 11 | fi 12 | done < $fn 13 | unset IFS 14 | } 15 | 16 | function flag_to_env_var() { 17 | prefix=$1 18 | flag=$2 19 | 20 | if [ "$prefix" == "" ] || [ "$flag" == "" ]; then 21 | return 1 22 | fi 23 | 24 | echo "${prefix}_${flag}" | tr "[a-z]" "[A-Z]" | tr "-" "_" 25 | return 0 26 | } 27 | 28 | function print_env_vars() { 29 | flags=("$@") 30 | prefix=${flags[0]} 31 | unset flags[0] 32 | for idx in ${!flags[@]}; do 33 | flag=${flags[idx]} 34 | env=$(flag_to_env_var "$prefix" "$flag") 35 | if [ $? -ne 0 ]; then 36 | continue 37 | fi 38 | echo "$flag || $env" 39 | done 40 | } 41 | 42 | # ack 's-cli:([^ ]*) ' --output '$1' sections.go 43 | function parse_env() { 44 | flags=("$@") 45 | prefix=${flags[0]} 46 | unset flags[0] 47 | 48 | if [ "$prefix" == "" ]; then 49 | return 1 50 | fi 51 | 52 | args="" 53 | for idx in ${!flags[@]}; do 54 | flag=${flags[idx]} 55 | env=$(flag_to_env_var "$prefix" "$flag") 56 | if [ $? -ne 0 ]; then 57 | continue 58 | fi 59 | 60 | if [ ! -z ${!env+x} ]; then 61 | args="${args} -${flag}=${!env}" 62 | fi 63 | done 64 | 65 | echo $args 66 | return 0 67 | } 68 | -------------------------------------------------------------------------------- /docker/util/clilist/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "strings" 9 | 10 | producer "github.com/splitio/split-synchronizer/v5/splitio/producer/conf" 11 | proxy "github.com/splitio/split-synchronizer/v5/splitio/proxy/conf" 12 | ) 13 | 14 | func main() { 15 | 16 | target := flag.String("target", "", "synchronizer|proxy") 17 | envPrefix := flag.String("env-prefix", "", "SPLIT_SYNC_ | SPLIT_PROXY_ | ...") 18 | output := flag.String("output", "{cli}\n", "string containing one or more of `cli,env,json,desc,type,default` in braces") 19 | flag.Parse() 20 | 21 | var config interface{} 22 | switch *target { 23 | case "synchronizer": 24 | config = producer.Main{} 25 | case "proxy": 26 | config = proxy.Main{} 27 | case "": 28 | fmt.Println("-target is required.") 29 | os.Exit(1) 30 | default: 31 | fmt.Println("invalid target config: ", *target) 32 | os.Exit(1) 33 | } 34 | 35 | parsedOutput := parseSpecialChars(*output) 36 | 37 | var collector OptionCollector 38 | VisitConfig(config, collector.Collect) 39 | 40 | for _, collected := range collector.collected { 41 | replacer := strings.NewReplacer( 42 | "{cli}", collected.CliArg, 43 | "{env}", *envPrefix + collected.Env, 44 | "{json}", collected.JSON, 45 | "{desc}", collected.Description, 46 | "{type}", collected.Type, 47 | "{default}", collected.Default, 48 | ) 49 | fmt.Print(replacer.Replace(parsedOutput)) 50 | } 51 | } 52 | 53 | type ConfigOption struct { 54 | CliArg string 55 | JSON string 56 | Env string 57 | Description string 58 | Type string 59 | Default string 60 | } 61 | 62 | type OptionCollector struct { 63 | collected []ConfigOption 64 | } 65 | 66 | func (o *OptionCollector) Collect(stack Stack, current reflect.StructField, value interface{}) bool { 67 | 68 | var cliOpt string 69 | stack.Each(func(f reflect.StructField) bool { 70 | if prefix, ok := f.Tag.Lookup("s-cli-prefix"); ok { 71 | cliOpt += prefix + "-" 72 | } 73 | return true 74 | }) 75 | cliOpt += current.Tag.Get("s-cli") 76 | 77 | jsonOpt := current.Name 78 | if j, ok := current.Tag.Lookup("json"); ok { 79 | jsonOpt = strings.Split(j, ",")[0] // remove `omitempty` and other stuff 80 | } 81 | 82 | o.collected = append(o.collected, ConfigOption{ 83 | CliArg: cliOpt, 84 | JSON: jsonOpt, 85 | Env: cliToEnv(cliOpt), 86 | Description: current.Tag.Get("s-desc"), 87 | Type: current.Type.Name(), 88 | Default: current.Tag.Get("s-def"), 89 | }) 90 | return true 91 | } 92 | 93 | func cliToEnv(cli string) string { 94 | return strings.ToUpper(strings.ReplaceAll(cli, "-", "_")) 95 | } 96 | 97 | func parseSpecialChars(s string) string { 98 | return strings.NewReplacer("\\n", "\n", "\\t", "\t").Replace(s) 99 | } 100 | 101 | type ConfigVisitor func(stack Stack, current reflect.StructField, value interface{}) (keepGoing bool) 102 | 103 | type Stack []reflect.StructField 104 | 105 | func (s Stack) Each(callback func(f reflect.StructField) bool) { 106 | for idx := 0; idx < len(s) && callback(s[idx]); idx++ { 107 | } 108 | } 109 | 110 | func VisitConfig(sp interface{}, visitor ConfigVisitor) { 111 | visitConfig(reflect.TypeOf(sp), reflect.ValueOf(sp), nil, visitor) 112 | } 113 | 114 | func visitConfig(rtype reflect.Type, val reflect.Value, stack Stack, visitor ConfigVisitor) { 115 | for _, field := range reflect.VisibleFields(rtype) { 116 | if field.Type.Kind() == reflect.Struct { 117 | stack = append(stack, field) 118 | visitConfig(field.Type, val.FieldByName(field.Name), stack, visitor) 119 | stack = stack[:len(stack)-1] 120 | } else { 121 | if !visitor(stack, field, val.Interface()) { 122 | return 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/splitio/split-synchronizer/v5 2 | 3 | go 1.23.9 4 | 5 | require ( 6 | github.com/gin-contrib/cors v1.6.0 7 | github.com/gin-contrib/gzip v0.0.6 8 | github.com/gin-gonic/gin v1.10.0 9 | github.com/google/uuid v1.3.0 10 | github.com/splitio/gincache v1.0.1 11 | github.com/splitio/go-split-commons/v6 v6.1.0 12 | github.com/splitio/go-toolkit/v5 v5.4.0 13 | github.com/stretchr/testify v1.9.0 14 | go.etcd.io/bbolt v1.3.6 15 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d 16 | ) 17 | 18 | require ( 19 | github.com/bits-and-blooms/bitset v1.3.1 // indirect 20 | github.com/bits-and-blooms/bloom/v3 v3.3.1 // indirect 21 | github.com/bytedance/sonic v1.11.6 // indirect 22 | github.com/bytedance/sonic/loader v0.1.1 // indirect 23 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 24 | github.com/cloudwego/base64x v0.1.4 // indirect 25 | github.com/cloudwego/iasm v0.2.0 // indirect 26 | github.com/davecgh/go-spew v1.1.1 // indirect 27 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 28 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 29 | github.com/gin-contrib/sse v0.1.0 // indirect 30 | github.com/go-playground/locales v0.14.1 // indirect 31 | github.com/go-playground/universal-translator v0.18.1 // indirect 32 | github.com/go-playground/validator/v10 v10.20.0 // indirect 33 | github.com/goccy/go-json v0.10.2 // indirect 34 | github.com/json-iterator/go v1.1.12 // indirect 35 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 36 | github.com/leodido/go-urn v1.4.0 // indirect 37 | github.com/mattn/go-isatty v0.0.20 // indirect 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 39 | github.com/modern-go/reflect2 v1.0.2 // indirect 40 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 41 | github.com/pmezard/go-difflib v1.0.0 // indirect 42 | github.com/redis/go-redis/v9 v9.7.3 // indirect 43 | github.com/stretchr/objx v0.5.2 // indirect 44 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 45 | github.com/ugorji/go/codec v1.2.12 // indirect 46 | golang.org/x/arch v0.17.0 // indirect 47 | golang.org/x/crypto v0.38.0 // indirect 48 | golang.org/x/net v0.40.0 // indirect 49 | golang.org/x/sync v0.14.0 // indirect 50 | golang.org/x/sys v0.33.0 // indirect 51 | golang.org/x/text v0.25.0 // indirect 52 | google.golang.org/protobuf v1.34.1 // indirect 53 | gopkg.in/yaml.v3 v3.0.1 // indirect 54 | ) 55 | -------------------------------------------------------------------------------- /release/commitversion.go.template: -------------------------------------------------------------------------------- 1 | package splitio 2 | 3 | /* 4 | This file is created automatically, please do not edit 5 | */ 6 | 7 | // CommitVersion is the version of the last commit previous to release 8 | const CommitVersion = "COMMIT_VERSION" 9 | -------------------------------------------------------------------------------- /release/install_script_template: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ------------------------------------------------------------ 4 | # Setup Environment 5 | # ------------------------------------------------------------ 6 | PATH=/usr/bin:/bin 7 | umask 022 8 | PDIR=${0%`basename $0`} 9 | ZIP_FILENAME=Unpacked.zip 10 | INSTALLER_VERSION=1.0.0 11 | SUCCESS_INSTALL=false 12 | 13 | # Number of lines in this script file (plus 1) 14 | SCRIPT_LINES=AUTO_REPLACE_SCRIPT_LINES 15 | 16 | # Run /bin/sum on your binary and put the two values here 17 | SUM1=AUTO_REPLACE_SUM1 18 | SUM2=AUTO_REPLACE_SUM2 19 | 20 | # ------------------------------------------------------------ 21 | # This is a trap so that the script can attempt to clean up 22 | # if it exits abnormally or when unexpected. 23 | # ------------------------------------------------------------ 24 | trap 'rm -f ${PDIR}/${ZIP_FILENAME}; exit 1' HUP INT QUIT TERM 25 | echo -e "\033[0;34m" 26 | echo -e " __ ____ _ _ _" 27 | echo -e " / /__ / ___| _ __ | (_) |_" 28 | echo -e " / / \ \ \___ \| '_ \| | | __|" 29 | echo -e " \ \ \ \ ___) | |_) | | | |_" 30 | echo -e " \_\ / / |____/| .__/|_|_|\__|" 31 | echo -e " /_/ |_|" 32 | echo -e "Split AUTO_REPLACE_APP_NAME - installer v${INSTALLER_VERSION}" 33 | echo -e "Commit version: AUTO_REPLACE_COMMIT_VERSION" 34 | echo -e "Build version: AUTO_REPLACE_BUILD_VERSION" 35 | echo -e "Split Software Inc." 36 | echo -e "\033[0m" 37 | 38 | if [ "$1" == "--version" ]; then 39 | exit 0 40 | fi 41 | 42 | # ------------------------------------------------------------ 43 | # Unpack the zip file (or binary) from the end of the script. 44 | # We do this with the tail command using the lines argument. 45 | # The (+) sign in front of the number of lines gives us the 46 | # number of lines in the file minus the number of lines 47 | # indicated. 48 | # ------------------------------------------------------------ 49 | echo "* Unpacking binary files..." 50 | tail -n +$SCRIPT_LINES "$0" > ${PDIR}/${ZIP_FILENAME} 51 | 52 | # ------------------------------------------------------------ 53 | # You could perform a checksum here on the unpacked zip file. 54 | # ------------------------------------------------------------ 55 | SUM=`sum ${PDIR}/${ZIP_FILENAME}` 56 | ASUM1=`echo "${SUM}" | awk '{print $1}'` 57 | ASUM2=`echo "${SUM}" | awk '{print $2}'` 58 | if [ ${ASUM1} -ne ${SUM1} ] || [ ${ASUM2} -ne ${SUM2} ]; then 59 | echo "The download file appears to be corrupted. Please download" 60 | echo "the file again and re-try the installation." 61 | exit 1 62 | fi 63 | 64 | # ------------------------------------------------------------ 65 | # Now you can extract the contents of your zip file and do 66 | # whatever other tasks suite your fancy. 67 | # ------------------------------------------------------------ 68 | unzip ${PDIR}/${ZIP_FILENAME} 69 | 70 | # ------------------------------------------------------------ 71 | # Installing binary 72 | # ------------------------------------------------------------ 73 | echo "* Installing AUTO_REPLACE_INSTALL_NAME binary in /usr/local/bin" 74 | if install AUTO_REPLACE_BIN_FILENAME /usr/local/bin/AUTO_REPLACE_INSTALL_NAME 75 | then SUCCESS_INSTALL=true 76 | fi 77 | 78 | # ------------------------------------------------------------ 79 | # Done / Cleanup 80 | # ------------------------------------------------------------ 81 | echo "* Deleting temporal files" 82 | rm -f ${PDIR}/${ZIP_FILENAME} 83 | rm -f AUTO_REPLACE_BIN_FILENAME 84 | 85 | if [ "$SUCCESS_INSTALL" = true ]; then 86 | echo "Split AUTO_REPLACE_APP_NAME has been installed." 87 | echo -e "Type \033[0;34mAUTO_REPLACE_INSTALL_NAME --help\033[0m for more information or visit https://split.io " 88 | else 89 | echo -e "\033[0;31mSomething were wrong on the installation.\033[0m" 90 | echo "Please try again, remember to check permissions or run it as root user." 91 | fi 92 | 93 | echo " " 94 | exit 0 95 | -------------------------------------------------------------------------------- /release/versions.css.tpl: -------------------------------------------------------------------------------- 1 | /* Space out content a bit */ 2 | body { 3 | padding-top: 20px; 4 | padding-bottom: 20px; 5 | font-family: 'Open Sans', sans-serif; 6 | } 7 | 8 | /* Everything but the jumbotron gets side spacing for mobile first views */ 9 | .header, 10 | .marketing, 11 | .footer { 12 | padding-right: 15px; 13 | padding-left: 15px; 14 | } 15 | 16 | /* Custom page header */ 17 | .header { 18 | padding-bottom: 20px; 19 | border-bottom: 1px solid #e5e5e5; 20 | } 21 | /* Make the masthead heading the same height as the navigation */ 22 | .header h3 { 23 | margin-top: 0; 24 | margin-bottom: 0; 25 | line-height: 40px; 26 | } 27 | 28 | /* Custom page footer */ 29 | .footer { 30 | padding-top: 19px; 31 | color: #777; 32 | border-top: 1px solid #e5e5e5; 33 | } 34 | 35 | .download-icon { 36 | font-size: 14pt; 37 | } 38 | 39 | .download-version { 40 | float: left; 41 | } 42 | 43 | table.table thead tr th.download-icon { 44 | font-size: 20pt; 45 | } 46 | 47 | /* Customize container */ 48 | @media (min-width: 768px) { 49 | .container { 50 | max-width: 730px; 51 | } 52 | } 53 | .container-narrow > hr { 54 | margin: 30px 0; 55 | } 56 | 57 | /* Main marketing message and sign up button */ 58 | .jumbotron { 59 | text-align: center; 60 | border-bottom: 0px; 61 | } 62 | /*.jumbotron .btn { 63 | padding: 14px 24px; 64 | font-size: 21px; 65 | }*/ 66 | 67 | /* Supporting marketing content */ 68 | .marketing { 69 | margin: 40px 0; 70 | } 71 | .marketing p + h4 { 72 | margin-top: 28px; 73 | } 74 | 75 | /* Responsive: Portrait tablets and up */ 76 | @media screen and (min-width: 768px) { 77 | /* Remove the padding we set earlier */ 78 | .header, 79 | .marketing, 80 | .footer { 81 | padding-right: 0; 82 | padding-left: 0; 83 | } 84 | /* Space out the masthead */ 85 | .header { 86 | margin-bottom: 30px; 87 | } 88 | /* Remove the bottom border on the jumbotron for visual effect */ 89 | .jumbotron { 90 | border-bottom: 0; 91 | background-color: #ffffff; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /release/versions.download-row.html.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {version} 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /release/versions.pos.html.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=split-synchronizer 2 | sonar.sources=. 3 | sonar.exclusions=docker/**,release/**,test/** 4 | sonar.tests=. 5 | sonar.test.inclusions=**/*_test.go 6 | sonar.go.coverage.reportPaths=coverage.out 7 | sonar.coverage.exclusions=\ 8 | **/main.go,\ 9 | **/mocks/**,\ 10 | splitio/admin/admin.go,\ 11 | splitio/admin/controllers/helpers.go,\ 12 | splitio/common/conf/advanced.go,\ 13 | splitio/common/conf/basic.go,\ 14 | splitio/common/errors.go,\ 15 | splitio/log/initialization.go,\ 16 | splitio/producer/initialization.go,\ 17 | splitio/proxy/internal/dtos.go,\ 18 | splitio/proxy/conf/sections.go,\ 19 | splitio/producer/conf/sections.go 20 | sonar.links.ci=https://github.com/splitio/split-synchronizer 21 | sonar.links.scm=https://github.com/splitio/split-synchronizer/actions 22 | -------------------------------------------------------------------------------- /splitio/admin/admin.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/splitio/go-toolkit/v5/logging" 9 | adminCommon "github.com/splitio/split-synchronizer/v5/splitio/admin/common" 10 | "github.com/splitio/split-synchronizer/v5/splitio/admin/controllers" 11 | "github.com/splitio/split-synchronizer/v5/splitio/common" 12 | cstorage "github.com/splitio/split-synchronizer/v5/splitio/common/storage" 13 | "github.com/splitio/split-synchronizer/v5/splitio/producer/evcalc" 14 | "github.com/splitio/split-synchronizer/v5/splitio/provisional/healthcheck/application" 15 | "github.com/splitio/split-synchronizer/v5/splitio/provisional/healthcheck/services" 16 | 17 | "github.com/gin-gonic/gin" 18 | ) 19 | 20 | const baseAdminPath = "/admin" 21 | const baseInfoPath = "/info" 22 | const baseShutdownPath = "/shutdown" 23 | 24 | // Options encapsulates dependencies & config options for the Admin server 25 | type Options struct { 26 | Host string 27 | Port int 28 | Name string 29 | Proxy bool 30 | Username string 31 | Password string 32 | Logger logging.LoggerInterface 33 | Storages adminCommon.Storages 34 | ImpressionsEvCalc evcalc.Monitor 35 | EventsEvCalc evcalc.Monitor 36 | Runtime common.Runtime 37 | HcAppMonitor application.MonitorIterface 38 | HcServicesMonitor services.MonitorIterface 39 | Snapshotter cstorage.Snapshotter 40 | TLS *tls.Config 41 | FullConfig interface{} 42 | FlagSpecVersion string 43 | LargeSegmentVersion string 44 | } 45 | 46 | type AdminServer struct { 47 | server *http.Server 48 | } 49 | 50 | // NewServer instantiates a new admin server 51 | func NewServer(options *Options) (*AdminServer, error) { 52 | router := gin.New() 53 | admin := router.Group(baseAdminPath) 54 | info := router.Group(baseInfoPath) 55 | shutdown := router.Group(baseShutdownPath) 56 | if options.Username != "" && options.Password != "" { 57 | admin = router.Group(baseAdminPath, gin.BasicAuth(gin.Accounts{options.Username: options.Password})) 58 | info = router.Group(baseInfoPath, gin.BasicAuth(gin.Accounts{options.Username: options.Password})) 59 | shutdown = router.Group(baseShutdownPath, gin.BasicAuth(gin.Accounts{options.Username: options.Password})) 60 | } 61 | 62 | dashboardController, err := controllers.NewDashboardController( 63 | options.Name, 64 | options.Proxy, 65 | options.Logger, 66 | options.Storages, 67 | options.ImpressionsEvCalc, 68 | options.EventsEvCalc, 69 | options.Runtime, 70 | options.HcAppMonitor, 71 | options.FlagSpecVersion, 72 | options.LargeSegmentVersion, 73 | ) 74 | if err != nil { 75 | return nil, fmt.Errorf("error instantiating dashboard controller: %w", err) 76 | } 77 | dashboardController.Register(admin) 78 | 79 | shutdownController := controllers.NewShutdownController(options.Runtime) 80 | shutdownController.Register(shutdown) 81 | 82 | healthcheckController := controllers.NewHealthCheckController( 83 | options.Logger, 84 | options.HcAppMonitor, 85 | options.HcServicesMonitor, 86 | ) 87 | healthcheckController.Register(router) 88 | 89 | infoController := controllers.NewInfoController(options.Proxy, options.Runtime, options.FullConfig) 90 | infoController.Register(info) 91 | 92 | observabilityController, err := controllers.NewObservabilityController(options.Proxy, options.Logger, options.Storages) 93 | if err != nil { 94 | return nil, fmt.Errorf("error instantiating observability controller: %w", err) 95 | } 96 | observabilityController.Register(admin) 97 | 98 | if options.Snapshotter != nil { 99 | snapshotController := controllers.NewSnapshotController(options.Logger, options.Snapshotter) 100 | snapshotController.Register(admin) 101 | } 102 | 103 | return &AdminServer{ 104 | server: &http.Server{ 105 | Addr: fmt.Sprintf("%s:%d", options.Host, options.Port), 106 | Handler: router, 107 | TLSConfig: options.TLS, 108 | }, 109 | }, nil 110 | } 111 | 112 | func (a *AdminServer) Start() error { 113 | if a.server.TLSConfig != nil { 114 | return a.server.ListenAndServeTLS("", "") // cert & key set in TLSConfig option 115 | } 116 | return a.server.ListenAndServe() 117 | } 118 | -------------------------------------------------------------------------------- /splitio/admin/common/config.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/splitio/go-split-commons/v6/storage" 4 | 5 | // Storages wraps storages in one struct 6 | type Storages struct { 7 | SplitStorage storage.SplitStorage 8 | SegmentStorage storage.SegmentStorage 9 | LocalTelemetryStorage storage.TelemetryRuntimeConsumer 10 | EventStorage storage.EventMultiSdkConsumer 11 | ImpressionStorage storage.ImpressionMultiSdkConsumer 12 | UniqueKeysStorage storage.UniqueKeysMultiSdkConsumer 13 | LargeSegmentStorage storage.LargeSegmentsStorage 14 | } 15 | -------------------------------------------------------------------------------- /splitio/admin/controllers/healthcheck.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/splitio/go-toolkit/v5/logging" 8 | "github.com/splitio/split-synchronizer/v5/splitio/provisional/healthcheck/application" 9 | "github.com/splitio/split-synchronizer/v5/splitio/provisional/healthcheck/services" 10 | ) 11 | 12 | // HealthCheckController description 13 | type HealthCheckController struct { 14 | logger logging.LoggerInterface 15 | appMonitor application.MonitorIterface 16 | dependenciesMonitor services.MonitorIterface 17 | } 18 | 19 | func (c *HealthCheckController) appHealth(ctx *gin.Context) { 20 | status := c.appMonitor.GetHealthStatus() 21 | if status.Healthy { 22 | ctx.JSON(http.StatusOK, status) 23 | return 24 | } 25 | ctx.JSON(http.StatusInternalServerError, status) 26 | } 27 | 28 | func (c *HealthCheckController) dependenciesHealth(ctx *gin.Context) { 29 | ctx.JSON(http.StatusOK, c.dependenciesMonitor.GetHealthStatus()) 30 | } 31 | 32 | // Register the dashboard endpoints 33 | func (c *HealthCheckController) Register(router gin.IRouter) { 34 | router.GET("/health/application", c.appHealth) 35 | router.GET("/health/dependencies", c.dependenciesHealth) 36 | } 37 | 38 | // NewHealthCheckController instantiates a new HealthCheck controller 39 | func NewHealthCheckController( 40 | logger logging.LoggerInterface, 41 | appMonitor application.MonitorIterface, 42 | dependenciesMonitor services.MonitorIterface, 43 | ) *HealthCheckController { 44 | return &HealthCheckController{ 45 | logger: logger, 46 | appMonitor: appMonitor, 47 | dependenciesMonitor: dependenciesMonitor, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /splitio/admin/controllers/healthcheck_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/splitio/go-toolkit/v5/logging" 12 | "github.com/splitio/split-synchronizer/v5/splitio/provisional/healthcheck/application" 13 | ) 14 | 15 | type monitorMock struct { 16 | statusCall func() application.HealthDto 17 | } 18 | 19 | func (m *monitorMock) GetHealthStatus() application.HealthDto { 20 | return m.statusCall() 21 | } 22 | 23 | func (m *monitorMock) NotifyEvent(counterType int) {} 24 | func (m *monitorMock) Reset(counterType int, value int) {} 25 | func (m *monitorMock) Start() {} 26 | func (m *monitorMock) Stop() {} 27 | 28 | func TestApplicationHealthCheckEndpointErr(t *testing.T) { 29 | 30 | appHC := &monitorMock{} 31 | appHC.statusCall = func() application.HealthDto { 32 | return application.HealthDto{ 33 | Healthy: false, 34 | } 35 | } 36 | 37 | ctrl := NewHealthCheckController(logging.NewLogger(nil), appHC, nil) 38 | 39 | resp := httptest.NewRecorder() 40 | ctx, router := gin.CreateTestContext(resp) 41 | ctrl.Register(router) 42 | 43 | ctx.Request, _ = http.NewRequest(http.MethodGet, "/health/application", nil) 44 | router.ServeHTTP(resp, ctx.Request) 45 | if resp.Code != 500 { 46 | t.Error("status code should be 500.") 47 | } 48 | 49 | responseBody, err := ioutil.ReadAll(resp.Body) 50 | if err != nil { 51 | t.Error(err) 52 | return 53 | } 54 | 55 | var result application.HealthDto 56 | if err := json.Unmarshal(responseBody, &result); err != nil { 57 | t.Error("there should be no error ", err) 58 | } 59 | } 60 | 61 | func TestApplicationHealthCheckEndpointOk(t *testing.T) { 62 | 63 | appHC := &monitorMock{} 64 | appHC.statusCall = func() application.HealthDto { 65 | return application.HealthDto{ 66 | Healthy: true, 67 | } 68 | } 69 | 70 | ctrl := NewHealthCheckController(logging.NewLogger(nil), appHC, nil) 71 | 72 | resp := httptest.NewRecorder() 73 | ctx, router := gin.CreateTestContext(resp) 74 | ctrl.Register(router) 75 | 76 | ctx.Request, _ = http.NewRequest(http.MethodGet, "/health/application", nil) 77 | router.ServeHTTP(resp, ctx.Request) 78 | if resp.Code != 200 { 79 | t.Error("status code should be 200.") 80 | } 81 | 82 | responseBody, err := ioutil.ReadAll(resp.Body) 83 | if err != nil { 84 | t.Error(err) 85 | return 86 | } 87 | 88 | var result application.HealthDto 89 | if err := json.Unmarshal(responseBody, &result); err != nil { 90 | t.Error("there should be no error ", err) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /splitio/admin/controllers/info.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/splitio/split-synchronizer/v5/splitio" 9 | "github.com/splitio/split-synchronizer/v5/splitio/common" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | // InfoController contains handlers for system information purposes 15 | type InfoController struct { 16 | proxy bool 17 | runtime common.Runtime 18 | cfg interface{} 19 | } 20 | 21 | // NewInfoController constructs a new InfoController to be mounted on a gin router 22 | func NewInfoController(proxy bool, runtime common.Runtime, config interface{}) *InfoController { 23 | return &InfoController{ 24 | proxy: proxy, 25 | runtime: runtime, 26 | cfg: config, 27 | } 28 | } 29 | 30 | // Register info controller endpoints 31 | func (c *InfoController) Register(router gin.IRouter) { 32 | router.GET("/uptime", c.uptime) 33 | router.GET("/version", c.version) 34 | router.GET("/ping", c.ping) 35 | router.GET("/config", c.config) 36 | } 37 | 38 | func (c *InfoController) config(ctx *gin.Context) { 39 | ctx.JSON(http.StatusOK, gin.H{"config": c.cfg}) 40 | } 41 | 42 | func (c *InfoController) uptime(ctx *gin.Context) { 43 | ctx.JSON(http.StatusOK, gin.H{"uptime": fmt.Sprintf("%s", c.runtime.Uptime().Round(time.Second))}) 44 | } 45 | 46 | func (c *InfoController) version(ctx *gin.Context) { 47 | ctx.JSON(http.StatusOK, gin.H{"version": splitio.Version}) 48 | } 49 | 50 | func (c *InfoController) ping(ctx *gin.Context) { 51 | ctx.String(http.StatusOK, "%s", "pong") 52 | } 53 | -------------------------------------------------------------------------------- /splitio/admin/controllers/observability.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/splitio/split-synchronizer/v5/splitio/admin/common" 7 | "github.com/splitio/split-synchronizer/v5/splitio/provisional/observability" 8 | pstorage "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage" 9 | 10 | "github.com/splitio/go-toolkit/v5/logging" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | type ObservabilityDto struct { 16 | ActiveSplits []string `json:"activeSplits"` 17 | ActiveSegments map[string]int `json:"activeSegments"` 18 | ActiveFlagSets []string `json:"activeFlagSets"` 19 | } 20 | 21 | // ObservabilityController interface is used to have a single constructor that returns the apropriate controller 22 | type ObservabilityController interface { 23 | Register(gin.IRouter) 24 | } 25 | 26 | // SyncObservabilityController exposes an observability endpoint exposing cached feature flags & segments information 27 | type SyncObservabilityController struct { 28 | logger logging.LoggerInterface 29 | splits observability.ObservableSplitStorage 30 | segments observability.ObservableSegmentStorage 31 | } 32 | 33 | // Register mounts the controller endpoints onto the supplied router 34 | func (c *SyncObservabilityController) Register(router gin.IRouter) { 35 | router.GET("/observability", c.observability) 36 | } 37 | 38 | func (c *SyncObservabilityController) observability(ctx *gin.Context) { 39 | ctx.JSON(200, ObservabilityDto{ 40 | ActiveSplits: c.splits.SplitNames(), 41 | ActiveSegments: c.segments.NamesAndCount(), 42 | ActiveFlagSets: c.splits.GetAllFlagSetNames(), 43 | }) 44 | } 45 | 46 | // ProxyObservabilityController exposes an observability endpoint exposing cached feature flags & segments information 47 | type ProxyObservabilityController struct { 48 | logger logging.LoggerInterface 49 | telemetry pstorage.TimeslicedProxyEndpointTelemetry 50 | splits observability.ObservableSplitStorage 51 | segments observability.ObservableSegmentStorage 52 | } 53 | 54 | // Register mounts the controller endpoints onto the supplied router 55 | func (c *ProxyObservabilityController) Register(router gin.IRouter) { 56 | router.GET("/observability", c.observability) 57 | } 58 | 59 | func (c *ProxyObservabilityController) observability(ctx *gin.Context) { 60 | ctx.JSON(200, gin.H{ 61 | "activeSplits": c.splits.SplitNames(), 62 | "activeSegments": c.segments.NamesAndCount(), 63 | "activeFlagSets": c.splits.GetAllFlagSetNames(), 64 | "proxyEndpointStats": c.telemetry.TimeslicedReport(), 65 | "proxyEndpointStatsTotal": c.telemetry.TotalMetricsReport(), 66 | }) 67 | } 68 | 69 | // NewObservabilityController constructs and returns the appropriate struct dependeing on whether the app is split-proxy or split-sync 70 | func NewObservabilityController(proxy bool, logger logging.LoggerInterface, storagePack common.Storages) (ObservabilityController, error) { 71 | 72 | splitStorage, ok := storagePack.SplitStorage.(observability.ObservableSplitStorage) 73 | if !ok { 74 | return nil, fmt.Errorf("invalid feature flag storage supplied: %T", storagePack.SplitStorage) 75 | } 76 | 77 | segmentStorage, ok := storagePack.SegmentStorage.(observability.ObservableSegmentStorage) 78 | if !ok { 79 | return nil, fmt.Errorf("invalid segment storage supplied: %T", storagePack.SegmentStorage) 80 | } 81 | 82 | if !proxy { 83 | return &SyncObservabilityController{ 84 | logger: logger, 85 | splits: splitStorage, 86 | segments: segmentStorage, 87 | }, nil 88 | 89 | } 90 | 91 | telemetry, ok := storagePack.LocalTelemetryStorage.(pstorage.TimeslicedProxyEndpointTelemetry) 92 | if !ok { 93 | return nil, fmt.Errorf("invalid local telemetry storage supplied: %T", storagePack.LocalTelemetryStorage) 94 | } 95 | 96 | return &ProxyObservabilityController{ 97 | logger: logger, 98 | splits: splitStorage, 99 | segments: segmentStorage, 100 | telemetry: telemetry, 101 | }, nil 102 | 103 | } 104 | -------------------------------------------------------------------------------- /splitio/admin/controllers/shutdown.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/splitio/split-synchronizer/v5/splitio/common" 8 | ) 9 | 10 | const ( 11 | forcedShutdown = "force" 12 | gracefulShutdown = "graceful" 13 | ) 14 | 15 | // ShutdownController bundles handlers that can shut down the synchronizer app 16 | type ShutdownController struct { 17 | runtime common.Runtime 18 | } 19 | 20 | // NewShutdownController instantiates a shutdown request handling controller 21 | func NewShutdownController(runtime common.Runtime) *ShutdownController { 22 | return &ShutdownController{runtime: runtime} 23 | } 24 | 25 | // Register mounts the endpoints 26 | func (c *ShutdownController) Register(router gin.IRouter) { 27 | router.GET("/stop/:stopType", c.stopProcess) 28 | } 29 | 30 | func (c *ShutdownController) stopProcess(ctx *gin.Context) { 31 | stopType := ctx.Param("stopType") 32 | var toReturn string 33 | 34 | switch stopType { 35 | case forcedShutdown: 36 | toReturn = stopType 37 | c.runtime.Kill() 38 | case gracefulShutdown: 39 | c.runtime.Shutdown() 40 | default: 41 | ctx.String(http.StatusBadRequest, "Invalid sign type: %s", toReturn) 42 | return 43 | } 44 | 45 | ctx.String(http.StatusOK, "%s: %s", "Signal has been sent", toReturn) 46 | 47 | } 48 | -------------------------------------------------------------------------------- /splitio/admin/controllers/snapshot.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/splitio/go-toolkit/v5/logging" 11 | "github.com/splitio/split-synchronizer/v5/splitio/common/snapshot" 12 | "github.com/splitio/split-synchronizer/v5/splitio/common/storage" 13 | ) 14 | 15 | // SnapshotController bundles endpoints associated to snapshot management 16 | type SnapshotController struct { 17 | logger logging.LoggerInterface 18 | db storage.Snapshotter 19 | } 20 | 21 | // NewSnapshotController constructs a new snapshot controller 22 | func NewSnapshotController(logger logging.LoggerInterface, db storage.Snapshotter) *SnapshotController { 23 | return &SnapshotController{logger: logger, db: db} 24 | } 25 | 26 | // Register mounts the endpoints int he provided router 27 | func (c *SnapshotController) Register(router gin.IRouter) { 28 | router.GET("/snapshot", c.downloadSnapshot) 29 | } 30 | 31 | func (c *SnapshotController) downloadSnapshot(ctx *gin.Context) { 32 | // curl http://localhost:3010/admin/proxy/snapshot --output split.proxy.0001.snapshot.gz 33 | snapshotName := fmt.Sprintf("split.proxy.%d.snapshot", time.Now().UnixNano()) 34 | b, err := c.db.GetRawSnapshot() 35 | if err != nil { 36 | c.logger.Error("error getting contents from db to build snapshot: ", err) 37 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": "error reading data"}) 38 | return 39 | } 40 | 41 | s, err := snapshot.New(snapshot.Metadata{Version: 1, Storage: snapshot.StorageBoltDB}, b) 42 | if err != nil { 43 | c.logger.Error("error building snapshot: ", err) 44 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": "error building snapshot"}) 45 | return 46 | } 47 | 48 | encodedSnap, err := s.Encode() 49 | if err != nil { 50 | c.logger.Error("error encoding snapshot: ", err) 51 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": "error encoding snapshot"}) 52 | return 53 | } 54 | 55 | ctx.Writer.Header().Set("Content-Type", "application/octet-stream") 56 | ctx.Writer.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, snapshotName)) 57 | ctx.Writer.Header().Set("Content-Length", strconv.Itoa(len(encodedSnap))) 58 | ctx.Writer.Write(encodedSnap) 59 | } 60 | -------------------------------------------------------------------------------- /splitio/admin/controllers/snapshot_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/splitio/go-toolkit/v5/logging" 12 | "github.com/splitio/split-synchronizer/v5/splitio/common/snapshot" 13 | "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage/persistent" 14 | ) 15 | 16 | func TestDownloadProxySnapshot(t *testing.T) { 17 | // Read DB snapshot for test 18 | path := "../../../test/snapshot/proxy.snapshot" 19 | snap, err := snapshot.DecodeFromFile(path) 20 | if err != nil { 21 | t.Error(err) 22 | return 23 | } 24 | 25 | tmpDataFile, err := snap.WriteDataToTmpFile() 26 | if err != nil { 27 | t.Error(err) 28 | return 29 | } 30 | 31 | // loading snapshot from disk 32 | dbInstance, err := persistent.NewBoltWrapper(tmpDataFile, nil) 33 | if err != nil { 34 | t.Error(err) 35 | return 36 | } 37 | 38 | ctrl := NewSnapshotController(logging.NewLogger(nil), dbInstance) 39 | 40 | resp := httptest.NewRecorder() 41 | ctx, router := gin.CreateTestContext(resp) 42 | ctrl.Register(router) 43 | 44 | ctx.Request, _ = http.NewRequest(http.MethodGet, "/snapshot", nil) 45 | router.ServeHTTP(resp, ctx.Request) 46 | 47 | responseBody, err := ioutil.ReadAll(resp.Body) 48 | if err != nil { 49 | t.Error(err) 50 | return 51 | } 52 | 53 | snapRes, err := snapshot.Decode(responseBody) 54 | if err != nil { 55 | t.Error(err) 56 | return 57 | } 58 | 59 | if snapRes.Meta().Version != 1 { 60 | t.Error("Invalid Metadata version") 61 | } 62 | 63 | if snapRes.Meta().Storage != 1 { 64 | t.Error("Invalid Metadata storage") 65 | } 66 | 67 | dat, err := snap.Data() 68 | if err != nil { 69 | t.Error(err) 70 | } 71 | resData, err := snapRes.Data() 72 | if err != nil { 73 | t.Error(err) 74 | } 75 | if bytes.Compare(dat, resData) != 0 { 76 | t.Error("loaded snapshot is different to downloaded") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /splitio/admin/views/dashboard/menu.go: -------------------------------------------------------------------------------- 1 | package dashboard 2 | 3 | const menu = ` 4 | {{define "Menu"}} 5 | 36 | {{end}} 37 | ` 38 | -------------------------------------------------------------------------------- /splitio/admin/views/dashboard/queuemanager.go: -------------------------------------------------------------------------------- 1 | package dashboard 2 | 3 | const queueManager = ` 4 | {{define "QueueManager"}} 5 |
6 | 7 |
8 |
9 |
10 |

Impressions Queue Size

11 |

12 |
13 |
14 |
15 |
16 |

Events Queue Size

17 |

18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 |

Impressions Lambda

26 |

27 |
28 |
29 |
30 |
31 |

Events Lambda

32 |

33 |
34 |
35 |
36 |
37 |
38 |
39 | 53 |
54 | {{end}} 55 | ` 56 | -------------------------------------------------------------------------------- /splitio/banner.go: -------------------------------------------------------------------------------- 1 | package splitio 2 | 3 | // ASCILogo for banners 4 | var ASCILogo = ` 5 | __ ____ _ _ _ 6 | / /__ / ___| _ __ | (_) |_ 7 | / / \ \ \___ \| '_ \| | | __| 8 | \ \ \ \ ___) | |_) | | | |_ 9 | \_\ / / |____/| .__/|_|_|\__| 10 | /_/ |_| 11 | 12 | ` 13 | -------------------------------------------------------------------------------- /splitio/commitversion.go: -------------------------------------------------------------------------------- 1 | package splitio 2 | 3 | /* 4 | This file is created automatically, please do not edit 5 | */ 6 | 7 | // CommitVersion is the version of the last commit previous to release 8 | const CommitVersion = "f8a1cde" 9 | -------------------------------------------------------------------------------- /splitio/common/conf/advanced.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/splitio/go-split-commons/v6/conf" 7 | ) 8 | 9 | // InitAdvancedOptions initializes an advanced config with default values + overriden urls. 10 | func InitAdvancedOptions(proxy bool) *conf.AdvancedConfig { 11 | advanced := conf.GetDefaultAdvancedConfig() 12 | 13 | prefix := "SPLIT_SYNC_" 14 | if proxy { 15 | prefix = "SPLIT_PROXY_" 16 | advanced.LargeSegment.Enable = true 17 | } 18 | 19 | if envSdkURL := os.Getenv(prefix + "SDK_URL"); envSdkURL != "" { 20 | advanced.SdkURL = envSdkURL 21 | } 22 | 23 | if envEventsURL := os.Getenv(prefix + "EVENTS_URL"); envEventsURL != "" { 24 | advanced.EventsURL = envEventsURL 25 | } 26 | 27 | if authServiceURL := os.Getenv(prefix + "AUTH_SERVICE_URL"); authServiceURL != "" { 28 | advanced.AuthServiceURL = authServiceURL 29 | } 30 | 31 | if streamingServiceURL := os.Getenv(prefix + "STREAMING_SERVICE_URL"); streamingServiceURL != "" { 32 | advanced.StreamingServiceURL = streamingServiceURL 33 | } 34 | 35 | if telemetryServiceURL := os.Getenv(prefix + "TELEMETRY_SERVICE_URL"); telemetryServiceURL != "" { 36 | advanced.TelemetryServiceURL = telemetryServiceURL 37 | } 38 | 39 | return &advanced 40 | } 41 | -------------------------------------------------------------------------------- /splitio/common/conf/basic.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "flag" 5 | ) 6 | 7 | // CliFlags defines the basic set of flags that are independent on the binary being executed & config required 8 | type CliFlags struct { 9 | ConfigFile *string 10 | WriteDefaultConfigFile *string 11 | VersionInfo *bool 12 | RawConfig ArgMap 13 | } 14 | 15 | // ParseCliArgs accepts a config options struct, parses it's definition (types + metadata) and builds the appropriate 16 | // flag definitions. It then parses the flags, and returns the structure filled with argument values 17 | func ParseCliArgs(definition interface{}) *CliFlags { 18 | flags := &CliFlags{ 19 | ConfigFile: flag.String("config", "", "a configuration file"), 20 | WriteDefaultConfigFile: flag.String("write-default-config", "", "write a default configuration file"), 21 | VersionInfo: flag.Bool("version", false, "Print the version"), 22 | RawConfig: MakeCliArgMapFor(definition), 23 | } 24 | 25 | flag.Parse() 26 | return flags 27 | } 28 | -------------------------------------------------------------------------------- /splitio/common/conf/file.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "reflect" 10 | 11 | validator "github.com/splitio/go-toolkit/v5/json-struct-validator" 12 | ) 13 | 14 | // ErrNoFile is the error to return when an empty config file si passed 15 | var ErrNoFile = errors.New("no config file provided") 16 | 17 | // PopulateConfigFromFile parses a json config file and populates the config struct passed as an argument 18 | func PopulateConfigFromFile(path string, target interface{}) error { 19 | if _, err := os.Stat(path); err != nil { 20 | return fmt.Errorf("error looking for config file (%s): %w", path, err) 21 | } 22 | 23 | data, err := ioutil.ReadFile(path) 24 | if err != nil { 25 | return fmt.Errorf("error reading config file (%s): %w", path, err) 26 | } 27 | 28 | err = json.Unmarshal(data, target) 29 | if err != nil { 30 | return fmt.Errorf("error parsing JSON config file (%s): %w", path, err) 31 | } 32 | 33 | // This function does a couple of things (to keep the caller clean): 34 | // - read the config file 35 | // - populate the struct with the appropriate values 36 | // - check that there are no extra fields in the json file 37 | // On top of that, the function needs to be generic, to work with different configs (sync || proxy). 38 | // The function needs a pointer to a struct to update it (modify it's contents) and it needs an actual struct 39 | // to inspect the fields. The `target` interface (which is already based on a pinter) parameter contains a pointer as well 40 | // to the struct being populated/validated. In order to validate, we need an `interface{}` object pointing to the same struct, 41 | // but without the extra indirection 42 | targetForValidation := reflect.Indirect(reflect.ValueOf(target)).Interface() 43 | err = validator.ValidateConfiguration(targetForValidation, data) 44 | if err != nil { 45 | return fmt.Errorf("error validating provided JSON file (%s): %w", path, err) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | // WriteDefaultConfigFile writes the default config defition to a JSON file 52 | func WriteDefaultConfigFile(name string, definition interface{}) error { 53 | if name == "" { 54 | return ErrNoFile 55 | } 56 | 57 | if err := PopulateDefaults(definition); err != nil { 58 | return fmt.Errorf("error populating defaults: %w", err) 59 | } 60 | 61 | data, err := json.MarshalIndent(definition, "", " ") 62 | if err != nil { 63 | return fmt.Errorf("error parsing definition: %w", err) 64 | } 65 | 66 | if err := ioutil.WriteFile(name, data, 0644); err != nil { 67 | return fmt.Errorf("error writing defaults to file: %w", err) 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /splitio/common/conf/sections.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | // Logging configuration options 4 | type Logging struct { 5 | Level string `json:"level" s-cli:"log-level" s-def:"info" s-desc:"Log level (error|warning|info|debug|verbose)"` 6 | Output string `json:"output" s-cli:"log-output" s-def:"stdout" s-desc:"Where to output logs (defaults to stdout)"` 7 | RotationMaxFiles int64 `json:"rotationMaxFiles" s-cli:"log-rotation-max-files" s-def:"10" s-desc:"Max number of files to keep when rotating logs"` 8 | RotationMaxSizeKb int64 `json:"rotationMaxSizeKb" s-cli:"log-rotation-max-size-kb" s-def:"1024" s-desc:"Maximum log file size in kbs"` 9 | } 10 | 11 | // Admin configuration options 12 | type Admin struct { 13 | Host string `json:"host" s-cli:"admin-host" s-def:"0.0.0.0" s-desc:"Host where the admin server will listen"` 14 | Port int64 `json:"port" s-cli:"admin-port" s-def:"3010" s-desc:"Admin port where incoming connections will be accepted"` 15 | Username string `json:"username" s-cli:"admin-username" s-def:"" s-desc:"HTTP basic auth username for admin endpoints"` 16 | Password string `json:"password" s-cli:"admin-password" s-def:"" s-desc:"HTTP basic auth password for admin endpoints"` 17 | SecureHC bool `json:"secureChecks" s-cli:"admin-secure-hc" s-def:"false" s-desc:"Secure Healthcheck endpoints as well."` 18 | TLS TLS `json:"tls" s-nested:"true" s-cli-prefix:"admin"` 19 | } 20 | 21 | // Integrations configuration options 22 | type Integrations struct { 23 | ImpressionListener ImpressionListener `json:"impressionListener" s-nested:"true"` 24 | Slack Slack `json:"slack" s-nested:"true"` 25 | } 26 | 27 | // ImpressionListener configuration options 28 | type ImpressionListener struct { 29 | Endpoint string `json:"endpoint" s-cli:"impression-listener-endpoint" s-def:"" s-desc:"HTTP endpoint to forward impressions to"` 30 | QueueSize int64 `json:"queueSize" s-cli:"impression-listener-queue-size" s-def:"100" s-desc:"max number of impressions bulks to queue"` 31 | } 32 | 33 | // Slack configuration options 34 | type Slack struct { 35 | Webhook string `json:"webhook" s-cli:"slack-webhook" s-def:"" s-desc:"slack webhook to post log messages"` 36 | Channel string `json:"channel" s-cli:"slack-channel" s-def:"" s-desc:"slack channel to post log messages"` 37 | } 38 | 39 | // TLS config options 40 | type TLS struct { 41 | Enabled bool `json:"enabled" s-cli:"tls-enabled" s-def:"false" s-desc:"Enable HTTPS on proxy endpoints"` 42 | ClientValidation bool `json:"clientValidation" s-cli:"tls-client-validation" s-def:"false" s-desc:"Enable client cert validation"` 43 | ServerName string `json:"serverName" s-cli:"tls-server-name" s-def:"" s-desc:"Server name as it appears in provided server-cert"` 44 | CertChainFN string `json:"certChainFn" s-cli:"tls-cert-chain-fn" s-def:"" s-desc:"X509 Server certificate chain"` 45 | PrivateKeyFN string `json:"privateKeyFn" s-cli:"tls-private-key-fn" s-def:"" s-desc:"PEM Private key file name"` 46 | ClientValidationRootCert string `json:"clientValidationRootCertFn" s-cli:"tls-client-validation-root-cert" s-def:"" s-desc:"X509 root cert for client validation"` 47 | MinTLSVersion string `json:"minTlsVersion" s-cli:"tls-min-tls-version" s-def:"1.3" s-desc:"Minimum TLS version to allow X.Y"` 48 | AllowedCipherSuites string `json:"allowedCipherSuites" s-cli:"tls-allowed-cipher-suites" s-def:"" s-desc:"Comma-separated list of cipher suites to allow"` 49 | } 50 | -------------------------------------------------------------------------------- /splitio/common/conf/validators.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/splitio/go-split-commons/v6/flagsets" 7 | ) 8 | 9 | type FlagSetValidationError struct { 10 | wrapped []error 11 | } 12 | 13 | func (f FlagSetValidationError) Error() string { 14 | var errors []string 15 | for _, err := range f.wrapped { 16 | errors = append(errors, err.Error()) 17 | } 18 | return strings.Join(errors, ".|| ") 19 | } 20 | 21 | func ValidateFlagsets(sets []string) ([]string, error) { 22 | var toRet error 23 | sanitizedFlagSets, fsErr := flagsets.SanitizeMany(sets) 24 | if fsErr != nil { 25 | toRet = FlagSetValidationError{wrapped: fsErr} 26 | } 27 | return sanitizedFlagSets, toRet 28 | } 29 | -------------------------------------------------------------------------------- /splitio/common/conf/validators_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splitio/go-split-commons/v6/dtos" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFlagSetValidationError(t *testing.T) { 11 | 12 | sanitized, err := ValidateFlagsets([]string{"Flagset1", " flagset2 ", "123#@flagset"}) 13 | assert.NotNil(t, err) 14 | assert.Equal(t, []string{"flagset1", "flagset2"}, sanitized) 15 | 16 | asFVE := err.(FlagSetValidationError) 17 | assert.Equal(t, 3, len(asFVE.wrapped)) 18 | assert.ElementsMatch(t, []error{ 19 | dtos.FlagSetValidatonError{Message: "Flag Set name Flagset1 should be all lowercase - converting string to lowercase"}, 20 | dtos.FlagSetValidatonError{Message: "Flag Set name flagset2 has extra whitespace, trimming"}, 21 | dtos.FlagSetValidatonError{Message: "you passed 123#@flagset, Flag Set must adhere to the regular expressions ^[a-z0-9][_a-z0-9]{0,49}$. This means a Flag Set must " + 22 | "start with a letter or number, be in lowercase, alphanumeric and have a max length of 50 characters. 123#@flagset was discarded."}, 23 | }, asFVE.wrapped) 24 | } 25 | -------------------------------------------------------------------------------- /splitio/common/errors.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Exit codes 8 | const ( 9 | ExitSuccess = iota 10 | ExitInvalidApikey 11 | ExitInvalidConfiguration 12 | ExitRedisInitializationFailed 13 | ExitErrorDB 14 | ExitTaskInitialization 15 | ExitAdminError 16 | ExitTLSError 17 | ExitUndefined 18 | ) 19 | 20 | // InitializationError wraps an error and an exit code 21 | type InitializationError struct { 22 | err error 23 | exitCode int 24 | } 25 | 26 | // NewInitError constructs an initialization error 27 | func NewInitError(err error, exitCode int) *InitializationError { 28 | return &InitializationError{ 29 | err: err, 30 | exitCode: exitCode, 31 | } 32 | } 33 | 34 | // Error returns the string representation of the error causing the initialization failure 35 | func (e *InitializationError) Error() string { 36 | return fmt.Sprintf("initialization error: %s", e.err) 37 | } 38 | 39 | // ExitCode is the number to return to the OS 40 | func (e *InitializationError) ExitCode() int { 41 | return e.exitCode 42 | } 43 | -------------------------------------------------------------------------------- /splitio/common/impressionlistener/dtos.go: -------------------------------------------------------------------------------- 1 | package impressionlistener 2 | 3 | // ImpressionForListener struct for payload 4 | type ImpressionForListener struct { 5 | KeyName string `json:"keyName"` 6 | Treatment string `json:"treatment"` 7 | Time int64 `json:"time"` 8 | ChangeNumber int64 `json:"changeNumber"` 9 | Label string `json:"label"` 10 | BucketingKey string `json:"bucketingKey,omitempty"` 11 | Pt int64 `json:"pt,omitempty"` 12 | } 13 | 14 | // ImpressionsForListener struct for payload 15 | type ImpressionsForListener struct { 16 | TestName string `json:"testName"` 17 | KeyImpressions []ImpressionForListener `json:"keyImpressions"` 18 | } 19 | -------------------------------------------------------------------------------- /splitio/common/impressionlistener/listener_test.go: -------------------------------------------------------------------------------- 1 | package impressionlistener 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/splitio/go-split-commons/v6/dtos" 11 | ) 12 | 13 | func TestImpressionListener(t *testing.T) { 14 | 15 | reqsDone := make(chan struct{}, 1) 16 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | defer func() { reqsDone <- struct{}{} }() 18 | if r.URL.Path != "/someUrl" && r.Method != "POST" { 19 | t.Error("Invalid request. Should be POST to /someUrl") 20 | } 21 | 22 | body, err := ioutil.ReadAll(r.Body) 23 | r.Body.Close() 24 | if err != nil { 25 | t.Error("Error reading body") 26 | return 27 | } 28 | 29 | var all impressionListenerPostBody 30 | err = json.Unmarshal(body, &all) 31 | if err != nil { 32 | t.Errorf("Error parsing json: %s", err) 33 | return 34 | } 35 | 36 | if all.SdkVersion != "go-1.1.1" || all.MachineIP != "1.2.3.4" || all.MachineName != "ip-1-2-3-4" { 37 | t.Error("invalid metadata") 38 | } 39 | 40 | imps := all.Impressions 41 | if len(imps) != 2 { 42 | t.Error("invalid number of impression groups received") 43 | return 44 | } 45 | 46 | if imps[0].TestName != "t1" || len(imps[0].KeyImpressions) != 2 { 47 | t.Errorf("invalid ipmressions for t1") 48 | return 49 | } 50 | 51 | if imps[1].TestName != "t2" || len(imps[1].KeyImpressions) != 2 { 52 | t.Errorf("invalid ipmressions for t2") 53 | return 54 | } 55 | })) 56 | defer ts.Close() 57 | 58 | listener, err := NewImpressionBulkListener(ts.URL, 10, nil) 59 | if err != nil { 60 | t.Error("error cannot be nil: ", err) 61 | } 62 | 63 | if err = listener.Start(); err != nil { 64 | t.Error("start() should not fail. Got: ", err) 65 | } 66 | defer listener.Stop(true) 67 | 68 | listener.Submit([]ImpressionsForListener{ 69 | ImpressionsForListener{ 70 | TestName: "t1", 71 | KeyImpressions: []ImpressionForListener{ 72 | ImpressionForListener{ 73 | KeyName: "k1", 74 | Treatment: "on", 75 | Time: 1, 76 | ChangeNumber: 2, 77 | Label: "l1", 78 | BucketingKey: "b1", 79 | Pt: 1, 80 | }, 81 | ImpressionForListener{ 82 | KeyName: "k2", 83 | Treatment: "on", 84 | Time: 1, 85 | ChangeNumber: 2, 86 | Label: "l1", 87 | BucketingKey: "b1", 88 | Pt: 1, 89 | }, 90 | }, 91 | }, 92 | ImpressionsForListener{ 93 | TestName: "t2", 94 | KeyImpressions: []ImpressionForListener{ 95 | ImpressionForListener{ 96 | KeyName: "k1", 97 | Treatment: "off", 98 | Time: 2, 99 | ChangeNumber: 3, 100 | Label: "l2", 101 | BucketingKey: "b2", 102 | Pt: 2, 103 | }, 104 | ImpressionForListener{ 105 | KeyName: "k2", 106 | Treatment: "off", 107 | Time: 2, 108 | ChangeNumber: 3, 109 | Label: "l2", 110 | BucketingKey: "b2", 111 | Pt: 3, 112 | }, 113 | }, 114 | }, 115 | }, &dtos.Metadata{SDKVersion: "go-1.1.1", MachineIP: "1.2.3.4", MachineName: "ip-1-2-3-4"}) 116 | 117 | <-reqsDone 118 | } 119 | -------------------------------------------------------------------------------- /splitio/common/impressionlistener/mocks/listener.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/splitio/go-split-commons/v6/dtos" 5 | "github.com/splitio/split-synchronizer/v5/splitio/common/impressionlistener" 6 | ) 7 | 8 | type ImpressionBulkListenerMock struct { 9 | SubmitCall func(imps []impressionlistener.ImpressionsForListener, metadata *dtos.Metadata) error 10 | StartCall func() error 11 | StopCall func(blocking bool) error 12 | } 13 | 14 | func (l *ImpressionBulkListenerMock) Submit(imps []impressionlistener.ImpressionsForListener, metadata *dtos.Metadata) error { 15 | return l.SubmitCall(imps, metadata) 16 | } 17 | 18 | func (l *ImpressionBulkListenerMock) Start() error { 19 | return l.StartCall() 20 | } 21 | 22 | func (l *ImpressionBulkListenerMock) Stop(blocking bool) error { 23 | return l.StopCall(blocking) 24 | } 25 | -------------------------------------------------------------------------------- /splitio/common/snapshot/snapshot_test.go: -------------------------------------------------------------------------------- 1 | package snapshot 2 | 3 | import "testing" 4 | 5 | func TestSnapshot(t *testing.T) { 6 | data4Test := []byte("Some Snapshot Data") 7 | storage4Test := uint64(4321) 8 | version4Test := uint64(123456) 9 | meta4Test := Metadata{Storage: storage4Test, Version: version4Test} 10 | 11 | snapshot, err := New(meta4Test, data4Test) 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | 16 | encoded, err := snapshot.Encode() 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | decodedSnapshot, err := Decode(encoded) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | if decodedSnapshot.Meta().Storage != storage4Test { 27 | t.Error("Metadata Storage invalid value") 28 | } 29 | 30 | if decodedSnapshot.Meta().Version != version4Test { 31 | t.Error("Metadata Version invalid value") 32 | } 33 | 34 | decodedData, err := decodedSnapshot.Data() 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | 39 | if string(decodedData) != string(data4Test) { 40 | t.Error("invalid decoded data") 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /splitio/common/storage/snapshotter.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | // Snapshotter interface to be implemented by storages that allow full retrieval of all raw data 4 | type Snapshotter interface { 5 | GetRawSnapshot() ([]byte, error) 6 | } 7 | -------------------------------------------------------------------------------- /splitio/common/sync/sync.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "github.com/splitio/go-toolkit/v5/logging" 5 | 6 | "github.com/splitio/go-split-commons/v6/conf" 7 | 8 | "github.com/splitio/go-split-commons/v6/synchronizer" 9 | "github.com/splitio/go-split-commons/v6/tasks" 10 | ) 11 | 12 | // WSync is a wrapper for the Regular synchronizer that handles both local telemetry 13 | // and user submitted telemetry 14 | type WSync struct { 15 | synchronizer.Synchronizer 16 | logger logging.LoggerInterface 17 | userTelemetryTasks []tasks.Task 18 | } 19 | 20 | // NewSynchronizer instantiates a producer-mode ready syncrhonizer that handles sdk-telemetry 21 | func NewSynchronizer( 22 | confAdvanced conf.AdvancedConfig, 23 | splitTasks synchronizer.SplitTasks, 24 | workers synchronizer.Workers, 25 | logger logging.LoggerInterface, 26 | inMememoryFullQueue chan string, 27 | userTelemetryTasks []tasks.Task, 28 | ) *WSync { 29 | return &WSync{ 30 | Synchronizer: synchronizer.NewSynchronizer(confAdvanced, splitTasks, workers, logger, inMememoryFullQueue), 31 | logger: logger, 32 | userTelemetryTasks: userTelemetryTasks, 33 | } 34 | } 35 | 36 | // StartPeriodicDataRecording starts periodic recorders tasks 37 | func (s *WSync) StartPeriodicDataRecording() { 38 | s.Synchronizer.StartPeriodicDataRecording() 39 | for _, t := range s.userTelemetryTasks { 40 | t.Start() 41 | } 42 | } 43 | 44 | // StopPeriodicDataRecording stops periodic recorders tasks 45 | func (s *WSync) StopPeriodicDataRecording() { 46 | s.Synchronizer.StopPeriodicDataRecording() 47 | for _, t := range s.userTelemetryTasks { 48 | t.Stop(true) 49 | } 50 | } 51 | 52 | // assert interface compliance 53 | var _ synchronizer.Synchronizer = (*WSync)(nil) 54 | -------------------------------------------------------------------------------- /splitio/enforce_fips.go: -------------------------------------------------------------------------------- 1 | //go:build enforce_fips 2 | // +build enforce_fips 3 | 4 | package splitio 5 | 6 | import ( 7 | _ "crypto/tls/fipsonly" 8 | ) 9 | -------------------------------------------------------------------------------- /splitio/log/custom_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splitio/go-toolkit/v5/testhelpers" 7 | ) 8 | 9 | func TestHistoricBuffer(t *testing.T) { 10 | hb := newHistoricBuffer(true, 3) 11 | hb.record("a") 12 | hb.record("b") 13 | hb.record("c") 14 | testhelpers.AssertStringSliceEquals(t, hb.messages(), []string{"a", "b", "c"}, "slices should match") 15 | 16 | hb.record("d") 17 | testhelpers.AssertStringSliceEquals(t, hb.messages(), []string{"b", "c", "d"}, "slices should match") 18 | 19 | if hb.count != 3 || hb.start != 1 || len(hb.buffer) != 3 { 20 | t.Error("incorrect values in vars ", hb.count, hb.start, len(hb.buffer)) 21 | } 22 | 23 | hb.record("e") 24 | testhelpers.AssertStringSliceEquals(t, hb.messages(), []string{"c", "d", "e"}, "slices should match") 25 | 26 | if hb.count != 3 || hb.start != 2 || len(hb.buffer) != 3 { 27 | t.Error("incorrect values in vars ", hb.count, hb.start, len(hb.buffer)) 28 | } 29 | 30 | hb.record("f") 31 | testhelpers.AssertStringSliceEquals(t, hb.messages(), []string{"d", "e", "f"}, "slices should match") 32 | 33 | if hb.count != 3 || hb.start != 0 || len(hb.buffer) != 3 { 34 | t.Error("incorrect values in vars ", hb.count, hb.start, len(hb.buffer)) 35 | } 36 | 37 | hb.record("g") 38 | testhelpers.AssertStringSliceEquals(t, hb.messages(), []string{"e", "f", "g"}, "slices should match") 39 | 40 | if hb.count != 3 || hb.start != 1 || len(hb.buffer) != 3 { 41 | t.Error("incorrect values in vars ", hb.count, hb.start, len(hb.buffer)) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /splitio/log/initialization.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "net/url" 9 | "os" 10 | "strings" 11 | 12 | "github.com/splitio/go-toolkit/v5/logging" 13 | "github.com/splitio/split-synchronizer/v5/splitio/common/conf" 14 | ) 15 | 16 | func meansStdout(s string) bool { 17 | switch strings.ToLower(s) { 18 | case "stdout", "/dev/stdout": 19 | return true 20 | default: 21 | return false 22 | } 23 | } 24 | 25 | // BuildFromConfig creates a logger from a config 26 | func BuildFromConfig(cfg *conf.Logging, prefix string, slackCfg *conf.Slack) *HistoricLoggerWrapper { 27 | var err error 28 | var mainWriter io.Writer = os.Stdout 29 | 30 | if !meansStdout(cfg.Output) { 31 | mainWriter, err = logging.NewFileRotate(&logging.FileRotateOptions{ 32 | MaxBytes: cfg.RotationMaxSizeKb * 1024, 33 | BackupCount: int(cfg.RotationMaxFiles), 34 | Path: cfg.Output, 35 | }) 36 | if err != nil { 37 | fmt.Printf("Error opening log output file: %s. Disabling logs!\n", err.Error()) 38 | mainWriter = ioutil.Discard 39 | } else { 40 | fmt.Printf("Log file: %s \n", cfg.Output) 41 | } 42 | } 43 | 44 | nonDebugWriter := mainWriter 45 | _, err = url.ParseRequestURI(slackCfg.Webhook) 46 | if err == nil && slackCfg.Channel != "" { 47 | nonDebugWriter = io.MultiWriter(mainWriter, NewSlackWriter(slackCfg.Webhook, slackCfg.Channel)) 48 | } 49 | 50 | var level int 51 | switch strings.ToUpper(cfg.Level) { 52 | case "VERBOSE": 53 | level = logging.LevelVerbose 54 | case "DEBUG": 55 | level = logging.LevelDebug 56 | case "INFO": 57 | level = logging.LevelInfo 58 | case "WARNING", "WARN": 59 | level = logging.LevelError 60 | case "ERROR": 61 | level = logging.LevelWarning 62 | case "NONE": 63 | level = logging.LevelNone 64 | } 65 | 66 | // buffer error, warning & info. don't buffer debug and verbose 67 | buffered := [5]bool{true, true, true, false, false} 68 | return NewHistoricLoggerWrapper(logging.NewLogger(&logging.LoggerOptions{ 69 | StandardLoggerFlags: log.Ldate | log.Ltime | log.Lshortfile, 70 | Prefix: prefix, 71 | VerboseWriter: mainWriter, 72 | DebugWriter: mainWriter, 73 | InfoWriter: nonDebugWriter, 74 | WarningWriter: nonDebugWriter, 75 | ErrorWriter: nonDebugWriter, 76 | LogLevel: level, 77 | ExtraFramesToSkip: 1, 78 | }), buffered, 5) 79 | } 80 | -------------------------------------------------------------------------------- /splitio/log/slack.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | // SlackWriter writes messages to Slack user or channel. Implements io.Writer interface 14 | type SlackWriter struct { 15 | webhookURL string 16 | httpClient http.Client 17 | channel string 18 | buffer chan []byte 19 | lastSent time.Time 20 | } 21 | 22 | // NewSlackWriter constructs a slack writer 23 | func NewSlackWriter(webhookURL string, channel string) *SlackWriter { 24 | toRet := &SlackWriter{ 25 | webhookURL: webhookURL, 26 | channel: channel, 27 | buffer: make(chan []byte, 200), 28 | } 29 | 30 | go toRet.poster() 31 | return toRet 32 | } 33 | 34 | // Write the message to slack webhook 35 | func (w *SlackWriter) Write(p []byte) (n int, err error) { 36 | message := make([]byte, len(p)) 37 | copy(message, p) 38 | 39 | select { 40 | case w.buffer <- message: 41 | default: 42 | // println a message? 43 | } 44 | return len(p), nil 45 | } 46 | 47 | func (w *SlackWriter) poster() { 48 | timer := time.NewTimer(500 * time.Millisecond) // TODO(mredolatti): make this configurable in final release 49 | localBuffer := make([][]byte, 0, 20) 50 | for { 51 | select { 52 | // TODO(mredolatti): add an exit path that flushes all messages on shutdown 53 | case message := <-w.buffer: 54 | localBuffer = append(localBuffer, message) 55 | case <-timer.C: 56 | for _, message := range localBuffer { 57 | w.postMessage(message, nil) 58 | } 59 | localBuffer = localBuffer[:0] // reset the slice without releasing/reallocating memory 60 | timer.Reset(500 * time.Millisecond) 61 | } 62 | } 63 | } 64 | 65 | func (w *SlackWriter) postMessage(msg []byte, attachements []SlackMessageAttachment) (err error) { 66 | message := messagePayload{ 67 | Channel: w.channel, 68 | Username: "Split-Sync", 69 | Text: string(msg), 70 | IconEmoji: ":robot_face:", 71 | Attachments: attachements, 72 | } 73 | 74 | serialized, err := json.Marshal(&message) 75 | if err != nil { 76 | return fmt.Errorf("error serializing message: %w", err) 77 | } 78 | 79 | req, _ := http.NewRequest("POST", w.webhookURL, bytes.NewBuffer(serialized)) 80 | resp, err := w.httpClient.Do(req) 81 | if err != nil { 82 | return fmt.Errorf("error posting log message to slack: %w", err) 83 | } 84 | defer resp.Body.Close() 85 | 86 | if resp.StatusCode >= 200 && resp.StatusCode < 400 { 87 | // If message has been written successfully (http 200 OK) 88 | return nil 89 | } 90 | body, _ := ioutil.ReadAll(resp.Body) 91 | return fmt.Errorf("Error posting log message to Slack %s, with message %s", resp.Status, body) 92 | } 93 | 94 | // PostNow post a message directly to slack channel 95 | func (w *SlackWriter) PostNow(msg []byte, attachements []SlackMessageAttachment) (err error) { 96 | return w.postMessage(msg, attachements) 97 | } 98 | 99 | type messagePayload struct { 100 | Channel string `json:"channel"` 101 | Username string `json:"username"` 102 | Text string `json:"text"` 103 | IconEmoji string `json:"icon_emoji"` 104 | Attachments []SlackMessageAttachment 105 | } 106 | 107 | // SlackMessageAttachmentFields attachment field struct 108 | type SlackMessageAttachmentFields struct { 109 | Title string `json:"title"` 110 | Value string `json:"value"` 111 | Short bool `json:"short"` 112 | } 113 | 114 | // SlackMessageAttachment attach message struct 115 | type SlackMessageAttachment struct { 116 | Fallback string `json:"fallback"` 117 | Text string `json:"text,omitempty"` 118 | Pretext string `json:"pretext,omitempty"` 119 | Color string `json:"color"` // Can either be one of 'good', 'warning', 'danger', or any hex color code 120 | Fields []SlackMessageAttachmentFields `json:"fields"` 121 | } 122 | 123 | var _ io.Writer = (*SlackWriter)(nil) 124 | -------------------------------------------------------------------------------- /splitio/producer/evcalc/evcalc.go: -------------------------------------------------------------------------------- 1 | package evcalc 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // Monitor specifies the interface for a lambda calculator 9 | type Monitor interface { 10 | StoreDataFlushed(timestamp time.Time, countFlushed int, countInStorage int64) 11 | Lambda() float64 12 | Acquire() bool 13 | Release() 14 | Busy() bool 15 | } 16 | 17 | // record struct that has all the required information of one flushing process 18 | type record struct { 19 | Timestamp time.Time 20 | DataFlushed int 21 | DataInStorage int64 22 | } 23 | 24 | // MonitorImpl struct that will has a window of statistics for eviction lambda calculation 25 | type MonitorImpl struct { 26 | flushingStats []record 27 | maxLength int 28 | mutex sync.RWMutex 29 | lambda float64 30 | busy bool 31 | } 32 | 33 | // New constructs a new eviction calculation monitor 34 | func New(threads int) *MonitorImpl { 35 | return &MonitorImpl{ 36 | flushingStats: make([]record, 0), 37 | maxLength: 100 * threads, 38 | lambda: 1, 39 | } 40 | } 41 | 42 | // StoreDataFlushed stores data flushed into the monitor 43 | func (m *MonitorImpl) StoreDataFlushed(timestamp time.Time, countFlushed int, countInStorage int64) { 44 | m.mutex.Lock() 45 | defer m.mutex.Unlock() 46 | 47 | if len(m.flushingStats) >= m.maxLength { 48 | m.flushingStats = m.flushingStats[1:m.maxLength] 49 | } 50 | m.flushingStats = append(m.flushingStats, record{ 51 | Timestamp: timestamp, 52 | DataFlushed: countFlushed, 53 | DataInStorage: countInStorage, 54 | }) 55 | m.lambda = m.calculateLambda() 56 | } 57 | 58 | // Lambda the returns the last known lambda value 59 | func (m *MonitorImpl) Lambda() float64 { 60 | m.mutex.RLock() 61 | defer m.mutex.RUnlock() 62 | return m.lambda 63 | } 64 | 65 | // Acquire requests permission to flush whichever resource this monitor is associated to 66 | func (m *MonitorImpl) Acquire() bool { 67 | m.mutex.Lock() 68 | defer m.mutex.Unlock() 69 | if m.busy { 70 | return false 71 | } 72 | 73 | m.busy = true 74 | return true 75 | } 76 | 77 | // Release signals the end of a syncrhonization operation which was previously acquired 78 | func (m *MonitorImpl) Release() { 79 | m.mutex.Lock() 80 | m.busy = false 81 | m.mutex.Unlock() 82 | } 83 | 84 | // Busy returns true if the permission is currently acquired and hasn't yet been released 85 | func (m *MonitorImpl) Busy() bool { 86 | m.mutex.RLock() 87 | defer m.mutex.RUnlock() 88 | return m.busy 89 | } 90 | 91 | func calculateAmountFlushed(records []record) int { 92 | amountFlushed := 0 93 | for _, i := range records { 94 | amountFlushed += i.DataFlushed 95 | } 96 | return amountFlushed 97 | } 98 | 99 | func (m *MonitorImpl) calculateLambda() float64 { 100 | t := int64(calculateAmountFlushed(m.flushingStats)) 101 | dataInT1 := m.flushingStats[0].DataInStorage 102 | dataInT2 := m.flushingStats[len(m.flushingStats)-1].DataInStorage 103 | amountGeneratedBetweenT1andT2 := float64(dataInT2 - dataInT1 + t) 104 | 105 | if amountGeneratedBetweenT1andT2 == 0 { 106 | return 1 107 | } 108 | return float64(t) / amountGeneratedBetweenT1andT2 109 | } 110 | 111 | var _ Monitor = (*MonitorImpl)(nil) 112 | -------------------------------------------------------------------------------- /splitio/producer/evcalc/evcalc_test.go: -------------------------------------------------------------------------------- 1 | package evcalc 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestEvictionCalculator(t *testing.T) { 9 | monitor := New(1) 10 | monitor.StoreDataFlushed(time.Now(), 100, 0) 11 | lambda := monitor.Lambda() 12 | if lambda != 1 { 13 | t.Error("Lambda should be 1 instead of ", lambda) 14 | } 15 | } 16 | 17 | func TestEvictionCalculatorWithInStorage(t *testing.T) { 18 | monitor := New(1) 19 | monitor.StoreDataFlushed(time.Now(), 100, 100) 20 | lambda := monitor.Lambda() 21 | if lambda != 1 { 22 | t.Error("Lambda should be 1 instead of", lambda) 23 | } 24 | 25 | if len(monitor.flushingStats) != 1 { 26 | t.Error("It should recorded 1") 27 | } 28 | } 29 | 30 | func TestEvictionCalculatorRegisteringTwo(t *testing.T) { 31 | monitor := New(1) 32 | monitor.StoreDataFlushed(time.Now(), 100, 100) 33 | monitor.StoreDataFlushed(time.Now(), 100, 0) 34 | lambda := monitor.Lambda() 35 | if lambda != 2 { 36 | t.Error("Lambda should be 1 instead of", lambda) 37 | } 38 | 39 | if len(monitor.flushingStats) != 2 { 40 | t.Error("It should recorded 2") 41 | } 42 | } 43 | 44 | func TestEvictionCalculatorWithMoreDataThatCanFlush(t *testing.T) { 45 | monitor := New(1) 46 | monitor.StoreDataFlushed(time.Now(), 100, 100) 47 | monitor.StoreDataFlushed(time.Now(), 100, 150) 48 | monitor.StoreDataFlushed(time.Now(), 100, 200) 49 | 50 | lambda := monitor.Lambda() 51 | if lambda != 0.75 { 52 | t.Error("Lambda should be 0.75 instead of", lambda) 53 | } 54 | 55 | if len(monitor.flushingStats) != 3 { 56 | t.Error("It should recorded 3") 57 | } 58 | } 59 | 60 | func TestEvictionCalculatorWithMoreDataThatCanFlushAndMoreDataThatCanStore(t *testing.T) { 61 | monitor := New(1) 62 | for i := 0; i < 120; i++ { 63 | monitor.StoreDataFlushed(time.Now(), 100, 100+(int64(i*10))) 64 | } 65 | 66 | lambda := monitor.Lambda() 67 | if lambda >= 1 { 68 | t.Error("Lambda should be less than 1") 69 | } 70 | 71 | if len(monitor.flushingStats) != 100 { 72 | t.Error("It should recorded 100") 73 | } 74 | } 75 | 76 | func TestNoDataReturns1(t *testing.T) { 77 | monitor := New(1) 78 | if monitor.Lambda() != 1 { 79 | t.Error("if no data has been submitted, lambda should be 1") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /splitio/producer/evcalc/mocks/evcalc.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import "time" 4 | 5 | type EvCalcMock struct { 6 | StoreDataFlushedCall func(timestamp time.Time, countFlushed int, countInStorage int64) 7 | LambdaCall func() float64 8 | AcquireCall func() bool 9 | ReleaseCall func() 10 | BusyCall func() bool 11 | } 12 | 13 | func (e *EvCalcMock) StoreDataFlushed(timestamp time.Time, countFlushed int, countInStorage int64) { 14 | e.StoreDataFlushedCall(timestamp, countFlushed, countInStorage) 15 | } 16 | 17 | func (e *EvCalcMock) Lambda() float64 { 18 | return e.LambdaCall() 19 | } 20 | 21 | func (e *EvCalcMock) Acquire() bool { 22 | return e.AcquireCall() 23 | } 24 | 25 | func (e *EvCalcMock) Release() { 26 | e.ReleaseCall() 27 | } 28 | 29 | func (e *EvCalcMock) Busy() bool { 30 | return e.BusyCall() 31 | } 32 | -------------------------------------------------------------------------------- /splitio/producer/storage/mocks/telemetry.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/splitio/go-split-commons/v6/dtos" 5 | ) 6 | 7 | // RedisTelemetryConsumerMultiMock is a mock 8 | type RedisTelemetryConsumerMultiMock struct { 9 | PopLatenciesCall func() map[dtos.Metadata]dtos.MethodLatencies 10 | PopExceptionsCall func() map[dtos.Metadata]dtos.MethodExceptions 11 | PopConfigsCall func() map[dtos.Metadata]dtos.Config 12 | } 13 | 14 | func (r *RedisTelemetryConsumerMultiMock) PopLatencies() map[dtos.Metadata]dtos.MethodLatencies { 15 | return r.PopLatenciesCall() 16 | } 17 | 18 | func (r *RedisTelemetryConsumerMultiMock) PopExceptions() map[dtos.Metadata]dtos.MethodExceptions { 19 | return r.PopExceptionsCall() 20 | } 21 | 22 | func (r *RedisTelemetryConsumerMultiMock) PopConfigs() map[dtos.Metadata]dtos.Config { 23 | return r.PopConfigsCall() 24 | } 25 | -------------------------------------------------------------------------------- /splitio/producer/task/impcounts.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "github.com/splitio/go-toolkit/v5/asynctask" 5 | "github.com/splitio/go-toolkit/v5/logging" 6 | "github.com/splitio/split-synchronizer/v5/splitio/producer/worker" 7 | ) 8 | 9 | func NewImpressionCountSyncTask( 10 | wrk worker.ImpressionsCounstWorkerImp, 11 | logger logging.LoggerInterface, 12 | period int, 13 | ) *asynctask.AsyncTask { 14 | doWork := func(l logging.LoggerInterface) error { 15 | wrk.Process() 16 | return nil 17 | } 18 | 19 | return asynctask.NewAsyncTask("sync-impression-counts", doWork, period, nil, nil, logger) 20 | } 21 | -------------------------------------------------------------------------------- /splitio/producer/task/telemetry.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "github.com/splitio/go-toolkit/v5/asynctask" 5 | "github.com/splitio/go-toolkit/v5/logging" 6 | "github.com/splitio/split-synchronizer/v5/splitio/producer/worker" 7 | ) 8 | 9 | // NewTelemetrySyncTask constructs a task used to periodically record sdk configs and stats into the Split servers 10 | func NewTelemetrySyncTask(wrk worker.TelemetryMultiWorker, logger logging.LoggerInterface, period int) *asynctask.AsyncTask { 11 | doWork := func(l logging.LoggerInterface) error { 12 | wrk.SynchronizeStats() 13 | wrk.SyncrhonizeConfigs() 14 | return nil 15 | } 16 | return asynctask.NewAsyncTask("sdk-telemetry", doWork, period, nil, nil, logger) 17 | } 18 | -------------------------------------------------------------------------------- /splitio/producer/task/uniquekeys.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/splitio/go-split-commons/v6/dtos" 10 | "github.com/splitio/go-split-commons/v6/provisional/strategy" 11 | "github.com/splitio/go-split-commons/v6/storage" 12 | "github.com/splitio/go-toolkit/v5/logging" 13 | ) 14 | 15 | // UniqueWorkerConfig bundles options 16 | type UniqueWorkerConfig struct { 17 | Logger logging.LoggerInterface 18 | Storage storage.UniqueKeysMultiSdkConsumer 19 | UniqueKeysTracker strategy.UniqueKeysTracker 20 | URL string 21 | Apikey string 22 | FetchSize int 23 | Metadata dtos.Metadata 24 | } 25 | 26 | // UniqueKeysPipelineWorker implements all the required methods to work with a pipelined task 27 | type UniqueKeysPipelineWorker struct { 28 | logger logging.LoggerInterface 29 | storage storage.UniqueKeysMultiSdkConsumer 30 | uniqueKeysTracker strategy.UniqueKeysTracker 31 | 32 | url string 33 | apikey string 34 | fetchSize int64 35 | metadata dtos.Metadata 36 | } 37 | 38 | func NewUniqueKeysWorker(cfg *UniqueWorkerConfig) Worker { 39 | return &UniqueKeysPipelineWorker{ 40 | logger: cfg.Logger, 41 | storage: cfg.Storage, 42 | uniqueKeysTracker: cfg.UniqueKeysTracker, 43 | url: cfg.URL + "/keys/ss", 44 | apikey: cfg.Apikey, 45 | fetchSize: int64(cfg.FetchSize), 46 | metadata: cfg.Metadata, 47 | } 48 | } 49 | 50 | func (u *UniqueKeysPipelineWorker) Fetch() ([]string, error) { 51 | raw, _, err := u.storage.PopNRaw(u.fetchSize) 52 | if err != nil { 53 | return nil, fmt.Errorf("error fetching raw unique keys: %w", err) 54 | } 55 | 56 | return raw, nil 57 | } 58 | 59 | func (u *UniqueKeysPipelineWorker) Process(raws [][]byte, sink chan<- interface{}) error { 60 | for _, raw := range raws { 61 | err, value := parseToObj(raw) 62 | if err == nil { 63 | u.logger.Debug("Unique Keys parsed to Dto.") 64 | } 65 | 66 | if err != nil { 67 | err, value = parseToArray(raw) 68 | if err != nil { 69 | u.logger.Error("error deserializing fetched uniqueKeys: ", err.Error()) 70 | continue 71 | } 72 | u.logger.Debug("Unique Keys parsed to Array.") 73 | } 74 | 75 | for _, unique := range value { 76 | for _, key := range unique.Keys { 77 | u.uniqueKeysTracker.Track(unique.Feature, key) 78 | } 79 | } 80 | } 81 | 82 | uniques := u.uniqueKeysTracker.PopAll() 83 | if len(uniques.Keys) > 0 { 84 | sink <- uniques 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (u *UniqueKeysPipelineWorker) BuildRequest(data interface{}) (*http.Request, error) { 91 | uniques, ok := data.(dtos.Uniques) 92 | if !ok { 93 | return nil, fmt.Errorf("expected uniqueKeys. Got: %T", data) 94 | } 95 | 96 | serialized, err := json.Marshal(uniques) 97 | req, err := http.NewRequest("POST", u.url, bytes.NewReader(serialized)) 98 | if err != nil { 99 | return nil, fmt.Errorf("error building unique keys post request: %w", err) 100 | } 101 | 102 | req.Header = http.Header{} 103 | req.Header.Add("Authorization", "Bearer "+u.apikey) 104 | req.Header.Add("Content-Type", "application/json") 105 | req.Header.Add("SplitSDKVersion", u.metadata.SDKVersion) 106 | req.Header.Add("SplitSDKMachineIp", u.metadata.MachineIP) 107 | req.Header.Add("SplitSDKMachineName", u.metadata.MachineName) 108 | return req, nil 109 | } 110 | 111 | func parseToArray(raw []byte) (error, []dtos.Key) { 112 | var queueObj []dtos.Key 113 | err := json.Unmarshal(raw, &queueObj) 114 | if err != nil { 115 | return err, nil 116 | } 117 | 118 | return nil, queueObj 119 | } 120 | 121 | func parseToObj(raw []byte) (error, []dtos.Key) { 122 | var queueObj dtos.Key 123 | err := json.Unmarshal(raw, &queueObj) 124 | if err != nil { 125 | return err, nil 126 | } 127 | 128 | return nil, []dtos.Key{queueObj} 129 | } 130 | -------------------------------------------------------------------------------- /splitio/producer/worker/impcounts.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "github.com/splitio/go-split-commons/v6/provisional/strategy" 5 | "github.com/splitio/go-split-commons/v6/storage" 6 | "github.com/splitio/go-toolkit/v5/logging" 7 | ) 8 | 9 | type ImpressionsCounstWorkerImp struct { 10 | impressionsCounter strategy.ImpressionsCounter 11 | storage storage.ImpressionsCountConsumer 12 | logger logging.LoggerInterface 13 | } 14 | 15 | func NewImpressionsCounstWorker( 16 | impressionsCounter strategy.ImpressionsCounter, 17 | storage storage.ImpressionsCountConsumer, 18 | logger logging.LoggerInterface, 19 | ) ImpressionsCounstWorkerImp { 20 | return ImpressionsCounstWorkerImp{ 21 | impressionsCounter: impressionsCounter, 22 | storage: storage, 23 | logger: logger, 24 | } 25 | } 26 | 27 | func (i *ImpressionsCounstWorkerImp) Process() error { 28 | impcounts, err := i.storage.GetImpressionsCount() 29 | if err != nil { 30 | return err 31 | } 32 | 33 | for _, count := range impcounts.PerFeature { 34 | i.impressionsCounter.Inc(count.FeatureName, count.TimeFrame, count.RawCount) 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /splitio/provisional/healthcheck/application/counter/basecounter.go: -------------------------------------------------------------------------------- 1 | package counter 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/splitio/go-toolkit/v5/logging" 8 | toolkitsync "github.com/splitio/go-toolkit/v5/sync" 9 | ) 10 | 11 | const ( 12 | // Critical severity 13 | Critical = iota 14 | // Low severity 15 | Low 16 | ) 17 | 18 | // HealthyResult description 19 | type HealthyResult struct { 20 | Name string 21 | Severity int 22 | Healthy bool 23 | LastHit *time.Time 24 | ErrorCount int 25 | } 26 | 27 | type applicationCounterImp struct { 28 | name string 29 | lastHit *time.Time 30 | healthy bool 31 | running toolkitsync.AtomicBool 32 | period int 33 | severity int 34 | lock sync.RWMutex 35 | logger logging.LoggerInterface 36 | } 37 | 38 | func (c *applicationCounterImp) updateLastHit() { 39 | now := time.Now() 40 | c.lastHit = &now 41 | } 42 | -------------------------------------------------------------------------------- /splitio/provisional/healthcheck/application/counter/periodic_test.go: -------------------------------------------------------------------------------- 1 | package counter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splitio/go-toolkit/v5/logging" 7 | ) 8 | 9 | func TestPeriodicCounter(t *testing.T) { 10 | 11 | steps := make(chan struct{}, 1) 12 | done := make(chan struct{}, 1) 13 | counter := NewPeriodicCounter(PeriodicConfig{ 14 | Name: "Test", 15 | Period: 2, 16 | MaxErrorsAllowedInPeriod: 2, 17 | Severity: 0, 18 | ValidationFunc: func(c PeriodicCounterInterface) { 19 | <-steps 20 | c.NotifyError() 21 | done <- struct{}{} 22 | <-steps 23 | c.NotifyError() 24 | done <- struct{}{} 25 | }, 26 | }, logging.NewLogger(nil)) 27 | 28 | counter.Start() 29 | 30 | if res := counter.IsHealthy(); !res.Healthy { 31 | t.Errorf("Healthy should be true") 32 | } 33 | 34 | steps <- struct{}{} 35 | <-done 36 | if res := counter.IsHealthy(); !res.Healthy { 37 | t.Errorf("Healthy should be true") 38 | } 39 | 40 | steps <- struct{}{} 41 | <-done 42 | if res := counter.IsHealthy(); res.Healthy { 43 | t.Errorf("Healthy should be false") 44 | } 45 | 46 | counter.lock.RLock() 47 | if counter.errorCount != 2 { 48 | t.Errorf("Errors should be 2") 49 | } 50 | counter.lock.RUnlock() 51 | 52 | counter.resetErrorCount() 53 | 54 | if res := counter.IsHealthy(); !res.Healthy { 55 | t.Errorf("Healthy should be true") 56 | } 57 | 58 | counter.lock.RLock() 59 | if counter.errorCount != 0 { 60 | t.Errorf("Errors should be 0") 61 | } 62 | counter.lock.RUnlock() 63 | counter.Stop() 64 | } 65 | -------------------------------------------------------------------------------- /splitio/provisional/healthcheck/application/counter/threshold.go: -------------------------------------------------------------------------------- 1 | package counter 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/splitio/go-toolkit/v5/logging" 9 | toolkitsync "github.com/splitio/go-toolkit/v5/sync" 10 | ) 11 | 12 | // ThresholdCounterInterface application counter interface 13 | type ThresholdCounterInterface interface { 14 | IsHealthy() HealthyResult 15 | NotifyHit() 16 | ResetThreshold(value int) error 17 | Start() 18 | Stop() 19 | } 20 | 21 | // ThresholdImp description 22 | type ThresholdImp struct { 23 | applicationCounterImp 24 | cancel chan struct{} 25 | reset chan struct{} 26 | } 27 | 28 | // ThresholdConfig config struct 29 | type ThresholdConfig struct { 30 | Name string 31 | Period int 32 | Severity int 33 | } 34 | 35 | // NotifyHit reset the timer 36 | func (c *ThresholdImp) NotifyHit() { 37 | if !c.running.IsSet() { 38 | c.logger.Debug(fmt.Sprintf("%s counter is not running.", c.name)) 39 | return 40 | } 41 | 42 | c.reset <- struct{}{} 43 | 44 | c.lock.Lock() 45 | defer c.lock.Unlock() 46 | 47 | c.updateLastHit() 48 | 49 | c.logger.Debug(fmt.Sprintf("event received for counter '%s'", c.name)) 50 | } 51 | 52 | // ResetThreshold the threshold value 53 | func (c *ThresholdImp) ResetThreshold(newThreshold int) error { 54 | if !c.running.IsSet() { 55 | c.logger.Warning(fmt.Sprintf("%s counter is not running.", c.name)) 56 | return nil 57 | } 58 | 59 | c.lock.Lock() 60 | defer c.lock.Unlock() 61 | 62 | if newThreshold <= 0 { 63 | return fmt.Errorf("refreshTreshold should be > 0") 64 | } 65 | 66 | c.period = newThreshold 67 | c.reset <- struct{}{} 68 | 69 | c.logger.Debug(fmt.Sprintf("updated threshold for counter '%s' to %d seconds", c.name, newThreshold)) 70 | 71 | return nil 72 | } 73 | 74 | // IsHealthy return the counter health 75 | func (c *ThresholdImp) IsHealthy() HealthyResult { 76 | c.lock.RLock() 77 | defer c.lock.RUnlock() 78 | 79 | return HealthyResult{ 80 | Name: c.name, 81 | Healthy: c.healthy, 82 | Severity: c.severity, 83 | LastHit: c.lastHit, 84 | } 85 | } 86 | 87 | // Start counter and timer 88 | func (c *ThresholdImp) Start() { 89 | if c.running.IsSet() { 90 | c.logger.Debug(fmt.Sprintf("%s counter is already running.", c.name)) 91 | return 92 | } 93 | 94 | c.lock.Lock() 95 | defer c.lock.Unlock() 96 | 97 | c.running.Set() 98 | 99 | go func() { 100 | c.lock.Lock() 101 | timer := time.NewTimer(time.Duration(c.period) * time.Second) 102 | c.lock.Unlock() 103 | 104 | defer timer.Stop() 105 | defer c.running.Unset() 106 | for { 107 | select { 108 | case <-timer.C: 109 | c.lock.Lock() 110 | c.logger.Error(fmt.Sprintf("counter '%s' has timed out with tolerance=%ds", c.name, c.period)) 111 | c.healthy = false 112 | c.lock.Unlock() 113 | return 114 | case <-c.reset: 115 | c.lock.Lock() 116 | timer.Reset(time.Duration(c.period) * time.Second) 117 | c.lock.Unlock() 118 | case <-c.cancel: 119 | return 120 | } 121 | } 122 | }() 123 | 124 | c.logger.Debug(fmt.Sprintf("%s threshold counter started.", c.name)) 125 | } 126 | 127 | // Stop counter 128 | func (c *ThresholdImp) Stop() { 129 | if !c.running.IsSet() { 130 | c.logger.Debug(fmt.Sprintf("%s counter is already stopped.", c.name)) 131 | return 132 | } 133 | 134 | c.lock.Lock() 135 | defer c.lock.Unlock() 136 | 137 | c.cancel <- struct{}{} 138 | } 139 | 140 | // NewThresholdCounter create Threshold counter 141 | func NewThresholdCounter( 142 | config ThresholdConfig, 143 | logger logging.LoggerInterface, 144 | ) *ThresholdImp { 145 | return &ThresholdImp{ 146 | applicationCounterImp: applicationCounterImp{ 147 | name: config.Name, 148 | lock: sync.RWMutex{}, 149 | logger: logger, 150 | healthy: true, 151 | running: *toolkitsync.NewAtomicBool(false), 152 | period: config.Period, 153 | severity: config.Severity, 154 | }, 155 | cancel: make(chan struct{}, 1), 156 | reset: make(chan struct{}, 1), 157 | } 158 | } 159 | 160 | // DefaultThresholdConfig new config with default values 161 | func DefaultThresholdConfig( 162 | name string, 163 | ) ThresholdConfig { 164 | return ThresholdConfig{ 165 | Name: name, 166 | Period: 3600, 167 | Severity: Critical, 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /splitio/provisional/healthcheck/application/counter/threshold_test.go: -------------------------------------------------------------------------------- 1 | package counter 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/splitio/go-toolkit/v5/logging" 8 | ) 9 | 10 | func TestThresholdCounter(t *testing.T) { 11 | counter := NewThresholdCounter(ThresholdConfig{ 12 | Name: "Test", 13 | Severity: 0, 14 | Period: 3, 15 | }, logging.NewLogger(nil)) 16 | counter.Start() 17 | 18 | counter.NotifyHit() 19 | res := counter.IsHealthy() 20 | if !res.Healthy { 21 | t.Errorf("Healthy should be true") 22 | } 23 | 24 | time.Sleep(time.Duration(1) * time.Second) 25 | 26 | counter.NotifyHit() 27 | res = counter.IsHealthy() 28 | if !res.Healthy { 29 | t.Errorf("Healthy should be true") 30 | } 31 | 32 | counter.ResetThreshold(1) 33 | 34 | time.Sleep(time.Duration(2) * time.Second) 35 | res = counter.IsHealthy() 36 | if res.Healthy { 37 | t.Errorf("Healthy should be false") 38 | } 39 | 40 | counter.Stop() 41 | } 42 | -------------------------------------------------------------------------------- /splitio/provisional/healthcheck/application/monitor_test.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/splitio/go-split-commons/v6/healthcheck/application" 8 | "github.com/splitio/go-toolkit/v5/logging" 9 | "github.com/splitio/split-synchronizer/v5/splitio/provisional/healthcheck/application/counter" 10 | ) 11 | 12 | func assertItemsHealthy(t *testing.T, items []ItemDto, splitsExpected bool, segmentsExpected bool, errorsExpected bool) { 13 | for _, item := range items { 14 | if item.Name == "Splits" && item.Healthy != splitsExpected { 15 | t.Errorf("SplitsCounter.Healthy should be %v", splitsExpected) 16 | } 17 | if item.Name == "Segments" && item.Healthy != segmentsExpected { 18 | t.Errorf("SegmentsCounter.Healthy should be %v", segmentsExpected) 19 | } 20 | if item.Name == "Sync-Errors" && item.Healthy != errorsExpected { 21 | t.Errorf("ErrorsCounter.Healthy should be %v", errorsExpected) 22 | } 23 | } 24 | } 25 | 26 | func TestMonitor(t *testing.T) { 27 | splitsCfg := counter.ThresholdConfig{ 28 | Name: "Splits", 29 | Period: 10, 30 | Severity: counter.Critical, 31 | } 32 | 33 | segmentsCfg := counter.ThresholdConfig{ 34 | Name: "Segments", 35 | Period: 10, 36 | Severity: counter.Critical, 37 | } 38 | 39 | lsCfg := counter.ThresholdConfig{ 40 | Name: "LargeSegments", 41 | Period: 10, 42 | Severity: counter.Critical, 43 | } 44 | 45 | storageCfg := counter.PeriodicConfig{ 46 | Name: "Storage", 47 | Period: 10, 48 | MaxErrorsAllowedInPeriod: 1, 49 | Severity: counter.Low, 50 | ValidationFunc: func(c counter.PeriodicCounterInterface) { 51 | c.NotifyError() 52 | }, 53 | } 54 | 55 | monitor := NewMonitorImp(splitsCfg, segmentsCfg, &lsCfg, &storageCfg, logging.NewLogger(nil)) 56 | 57 | monitor.Start() 58 | 59 | time.Sleep(time.Duration(1) * time.Second) 60 | 61 | res := monitor.GetHealthStatus() 62 | if !res.Healthy { 63 | t.Errorf("Healthy should be true") 64 | } 65 | 66 | assertItemsHealthy(t, res.Items, true, true, false) 67 | 68 | monitor.NotifyEvent(application.Splits) 69 | monitor.NotifyEvent(application.Segments) 70 | 71 | res = monitor.GetHealthStatus() 72 | if !res.Healthy { 73 | t.Errorf("Healthy should be true") 74 | } 75 | 76 | assertItemsHealthy(t, res.Items, true, true, false) 77 | 78 | monitor.Reset(application.Splits, 1) 79 | 80 | time.Sleep(time.Duration(2) * time.Second) 81 | res = monitor.GetHealthStatus() 82 | if res.Healthy { 83 | t.Errorf("Healthy should be false") 84 | } 85 | 86 | assertItemsHealthy(t, res.Items, false, true, false) 87 | monitor.Stop() 88 | } 89 | -------------------------------------------------------------------------------- /splitio/provisional/healthcheck/services/counter/bypercentage_test.go: -------------------------------------------------------------------------------- 1 | package counter 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/splitio/go-toolkit/v5/logging" 9 | ) 10 | 11 | func TestNotifyHitByPercentage(t *testing.T) { 12 | c := ByPercentageImp{ 13 | name: "TestCounter", 14 | lock: sync.RWMutex{}, 15 | logger: logging.NewLogger(nil), 16 | severity: 1, 17 | healthy: true, 18 | maxLen: 6, 19 | cache: new(list.List), 20 | percentageToBeHealthy: 60, 21 | } 22 | 23 | res := c.IsHealthy() 24 | if res.Healthy != true { 25 | t.Errorf("Health should be true") 26 | } 27 | 28 | if res.LastMessage != "" { 29 | t.Errorf("LastMessage should be empty") 30 | } 31 | 32 | if res.Severity != 1 { 33 | t.Errorf("Severity should be 1") 34 | } 35 | 36 | c.NotifyHit(500, "Error-1") 37 | res = c.IsHealthy() 38 | if res.Healthy != false { 39 | t.Errorf("Health should be false") 40 | } 41 | 42 | if res.LastMessage != "Error-1" { 43 | t.Errorf("LastMessage should be Error-1") 44 | } 45 | 46 | c.NotifyHit(500, "Error-2") 47 | res = c.IsHealthy() 48 | if res.Healthy != false { 49 | t.Errorf("Health should be false") 50 | } 51 | 52 | if res.LastMessage != "Error-2" { 53 | t.Errorf("LastMessage should be Error-2") 54 | } 55 | 56 | c.NotifyHit(200, "") 57 | c.NotifyHit(200, "") 58 | res = c.IsHealthy() 59 | if res.Healthy != false { 60 | t.Errorf("Health should be false") 61 | } 62 | 63 | if res.LastMessage != "Error-2" { 64 | t.Errorf("LastMessage should be Error-2") 65 | } 66 | 67 | c.NotifyHit(200, "") 68 | res = c.IsHealthy() 69 | if res.Healthy != true { 70 | t.Errorf("Health should be true") 71 | } 72 | 73 | if res.LastMessage != "" { 74 | t.Errorf("LastMessage should be empty. %s", res.LastMessage) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /splitio/provisional/healthcheck/services/monitor.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/splitio/go-toolkit/v5/logging" 8 | "github.com/splitio/split-synchronizer/v5/splitio/provisional/healthcheck/services/counter" 9 | ) 10 | 11 | const ( 12 | healthyStatus = "healthy" 13 | downStatus = "down" 14 | degradedStatus = "degraded" 15 | ) 16 | 17 | // HealthDto description 18 | type HealthDto struct { 19 | Status string `json:"serviceStatus"` 20 | Items []ItemDto `json:"dependencies"` 21 | } 22 | 23 | // ItemDto description 24 | type ItemDto struct { 25 | Service string `json:"service"` 26 | Healthy bool `json:"healthy"` 27 | Message string `json:"message,omitempty"` 28 | HealthySince *time.Time `json:"healthySince,omitempty"` 29 | LastHit *time.Time `json:"lastHit,omitempty"` 30 | } 31 | 32 | // MonitorIterface monitor interface 33 | type MonitorIterface interface { 34 | Start() 35 | Stop() 36 | GetHealthStatus() HealthDto 37 | } 38 | 39 | // MonitorImp description 40 | type MonitorImp struct { 41 | Counters []counter.ServicesCounterInterface 42 | lock sync.RWMutex 43 | logger logging.LoggerInterface 44 | } 45 | 46 | // Start stop counters 47 | func (m *MonitorImp) Start() { 48 | m.lock.Lock() 49 | defer m.lock.Unlock() 50 | 51 | for _, c := range m.Counters { 52 | c.Start() 53 | } 54 | } 55 | 56 | // Stop stop counters 57 | func (m *MonitorImp) Stop() { 58 | m.lock.Lock() 59 | defer m.lock.Unlock() 60 | 61 | for _, c := range m.Counters { 62 | c.Stop() 63 | } 64 | } 65 | 66 | // GetHealthStatus return services health 67 | func (m *MonitorImp) GetHealthStatus() HealthDto { 68 | m.lock.RLock() 69 | defer m.lock.RUnlock() 70 | 71 | var items []ItemDto 72 | 73 | criticalCount := 0 74 | degradedCount := 0 75 | 76 | for _, c := range m.Counters { 77 | res := c.IsHealthy() 78 | 79 | if !res.Healthy { 80 | switch res.Severity { 81 | case counter.Critical: 82 | criticalCount++ 83 | case counter.Degraded: 84 | degradedCount++ 85 | } 86 | } 87 | 88 | items = append(items, ItemDto{ 89 | Service: res.URL, 90 | Healthy: res.Healthy, 91 | Message: res.LastMessage, 92 | HealthySince: res.HealthySince, 93 | LastHit: res.LastHit, 94 | }) 95 | } 96 | 97 | status := healthyStatus 98 | 99 | if criticalCount > 0 { 100 | status = downStatus 101 | } else if degradedCount > 0 { 102 | status = degradedStatus 103 | } 104 | 105 | return HealthDto{ 106 | Status: status, 107 | Items: items, 108 | } 109 | } 110 | 111 | // NewMonitorImp create services monitor 112 | func NewMonitorImp( 113 | cfgs []counter.Config, 114 | logger logging.LoggerInterface, 115 | ) *MonitorImp { 116 | var serviceCounters []counter.ServicesCounterInterface 117 | 118 | for _, cfg := range cfgs { 119 | serviceCounters = append(serviceCounters, counter.NewCounterByPercentage(cfg, logger)) 120 | } 121 | 122 | return &MonitorImp{ 123 | Counters: serviceCounters, 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /splitio/provisional/healthcheck/services/monitor_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splitio/go-toolkit/v5/logging" 7 | "github.com/splitio/split-synchronizer/v5/splitio/provisional/healthcheck/services/counter" 8 | ) 9 | 10 | func TestGetHealthStatusByPercentage(t *testing.T) { 11 | var serviceCounters []counter.ServicesCounterInterface 12 | 13 | eventsConfig := counter.Config{ 14 | Name: "EVENTS", 15 | ServiceURL: "https://events.test.io/api", 16 | ServiceHealthEndpoint: "/version", 17 | TaskPeriod: 100, 18 | MaxLen: 2, 19 | PercentageToBeHealthy: 100, 20 | Severity: counter.Critical, 21 | } 22 | 23 | criticalCounter := counter.NewCounterByPercentage(eventsConfig, logging.NewLogger(nil)) 24 | 25 | streamingConfig := counter.Config{ 26 | Name: "STREAMING", 27 | ServiceURL: "https://streaming.test.io", 28 | ServiceHealthEndpoint: "/health", 29 | TaskPeriod: 100, 30 | MaxLen: 2, 31 | PercentageToBeHealthy: 100, 32 | Severity: counter.Degraded, 33 | } 34 | 35 | degradedCounter := counter.NewCounterByPercentage(streamingConfig, logging.NewLogger(nil)) 36 | 37 | serviceCounters = append(serviceCounters, criticalCounter, degradedCounter) 38 | 39 | m := MonitorImp{ 40 | Counters: serviceCounters, 41 | } 42 | 43 | res := m.GetHealthStatus() 44 | 45 | if res.Status != healthyStatus { 46 | t.Errorf("Status should be healthy - Actual status: %s", res.Status) 47 | } 48 | 49 | degradedCounter.NotifyHit(500, "message error") 50 | 51 | res = m.GetHealthStatus() 52 | 53 | if res.Status != degradedStatus { 54 | t.Errorf("Status should be degraded") 55 | } 56 | 57 | criticalCounter.NotifyHit(500, "message error") 58 | 59 | res = m.GetHealthStatus() 60 | 61 | if res.Status != downStatus { 62 | t.Errorf("Status should be down") 63 | } 64 | 65 | criticalCounter.NotifyHit(200, "") 66 | criticalCounter.NotifyHit(200, "") 67 | 68 | res = m.GetHealthStatus() 69 | 70 | if res.Status != degradedStatus { 71 | t.Errorf("Status should be degraded - Actual status: %s", res.Status) 72 | } 73 | 74 | degradedCounter.NotifyHit(200, "") 75 | 76 | res = m.GetHealthStatus() 77 | 78 | if res.Status != degradedStatus { 79 | t.Errorf("Status should be degraded - Actual status: %s", res.Status) 80 | } 81 | 82 | degradedCounter.NotifyHit(200, "") 83 | 84 | res = m.GetHealthStatus() 85 | 86 | if res.Status != healthyStatus { 87 | t.Errorf("Status should be healthy - Actual status: %s", res.Status) 88 | } 89 | 90 | degradedCounter.NotifyHit(200, "") 91 | 92 | res = m.GetHealthStatus() 93 | 94 | if res.Status != healthyStatus { 95 | t.Errorf("Status should be healthy - Actual status: %s", res.Status) 96 | } 97 | 98 | degradedCounter.NotifyHit(200, "") 99 | 100 | res = m.GetHealthStatus() 101 | 102 | if res.Status != healthyStatus { 103 | t.Errorf("Status should be healthy - Actual status: %s", res.Status) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /splitio/provisional/observability/split_wrapper_test.go: -------------------------------------------------------------------------------- 1 | package observability 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/splitio/go-split-commons/v6/dtos" 8 | "github.com/splitio/go-split-commons/v6/storage/mocks" 9 | "github.com/splitio/go-split-commons/v6/storage/redis" 10 | "github.com/splitio/go-toolkit/v5/logging" 11 | ) 12 | 13 | func TestSplitWrapper(t *testing.T) { 14 | st := &extMockSplitStorage{ 15 | &mocks.MockSplitStorage{ 16 | SplitNamesCall: func() []string { 17 | return []string{"split1", "split2"} 18 | }, 19 | UpdateCall: func([]dtos.SplitDTO, []dtos.SplitDTO, int64) { 20 | t.Error("should not be called") 21 | }, 22 | }, 23 | nil, 24 | } 25 | 26 | observer, _ := NewObservableSplitStorage(st, logging.NewLogger(nil)) 27 | if c := observer.Count(); c != 2 { 28 | t.Error("count sohuld be 2. Is ", c) 29 | } 30 | 31 | // split4 should fail to be updated 32 | st.UpdateWithErrorsCall = func(toAdd []dtos.SplitDTO, toRemove []dtos.SplitDTO, cn int64) error { 33 | return &redis.UpdateError{ 34 | FailedToAdd: map[string]error{"split4": errors.New("something")}, 35 | } 36 | } 37 | observer.Update([]dtos.SplitDTO{{Name: "split3"}, {Name: "split4"}}, []dtos.SplitDTO{}, 1) 38 | if _, ok := observer.active.activeSplitMap["split3"]; !ok { 39 | t.Error("split3 should be cached") 40 | } 41 | if _, ok := observer.active.activeSplitMap["split4"]; ok { 42 | t.Error("split4 should not be cached") 43 | } 44 | } 45 | 46 | var errSome = errors.New("some random error") 47 | 48 | func TestActiveSplitTracker(t *testing.T) { 49 | trk := newActiveSplitTracker(10) 50 | trk.update([]string{"split1", "split2"}, []string{"nonexistant"}) 51 | n := trk.names() 52 | if len(n) != 2 || trk.count() != 2 { 53 | t.Error("there should be 2 elements") 54 | } 55 | 56 | trk.update(nil, []string{"split2"}) 57 | if trk.names()[0] != "split1" { 58 | t.Error("there should be only one element 'split1'") 59 | } 60 | 61 | trk.update(nil, []string{"split1"}) 62 | if len(trk.names()) != 0 || trk.count() != 0 { 63 | t.Error("there should be 0 items") 64 | } 65 | } 66 | 67 | func TestFilterFailed(t *testing.T) { 68 | splits := []dtos.SplitDTO{{Name: "split1"}, {Name: "split2"}, {Name: "split3"}, {Name: "split4"}} 69 | failed := map[string]error{ 70 | "split2": errSome, 71 | "split4": errSome, 72 | } 73 | 74 | withoutFailed := filterFailed(splits, failed) 75 | if l := len(withoutFailed); l != 2 { 76 | t.Error("there should be 2 items after removing the failed ones. Have: ", l) 77 | t.Error(withoutFailed) 78 | } 79 | 80 | if n := withoutFailed[0].Name; n != "split1" { 81 | t.Error("first one should be 'split1'. is: ", n) 82 | } 83 | if n := withoutFailed[1].Name; n != "split3" { 84 | t.Error("first one should be 'split1'. is: ", n) 85 | } 86 | } 87 | 88 | type extMockSplitStorage struct { 89 | *mocks.MockSplitStorage 90 | UpdateWithErrorsCall func([]dtos.SplitDTO, []dtos.SplitDTO, int64) error 91 | } 92 | 93 | func (e *extMockSplitStorage) UpdateWithErrors(toAdd []dtos.SplitDTO, toRemove []dtos.SplitDTO, cn int64) error { 94 | return e.UpdateWithErrorsCall(toAdd, toRemove, cn) 95 | } 96 | 97 | var _ extendedSplitStorage = (*extMockSplitStorage)(nil) 98 | -------------------------------------------------------------------------------- /splitio/proxy/caching/caching.go: -------------------------------------------------------------------------------- 1 | package caching 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/splitio/gincache" 8 | "github.com/splitio/go-split-commons/v6/dtos" 9 | ) 10 | 11 | const ( 12 | // SurrogateContextKey is the gin context key used to store surrogates generated on each response 13 | SurrogateContextKey = "surrogates" 14 | 15 | // StickyContextKey should be set to (boolean) true whenever we want an entry to be kept in cache when making room 16 | // for new entries 17 | StickyContextKey = gincache.StickyEntry 18 | 19 | // SplitSurrogate key (we only need one, since all splitChanges should be expired when an update is processed) 20 | SplitSurrogate = "sp" 21 | 22 | // MembershipsSurrogate key (we only need one, since all memberships should be expired when an update is processed) 23 | MembershipsSurrogate = "mem" 24 | 25 | // AuthSurrogate key (having push disabled, it's safe to cache this and return it on all requests) 26 | AuthSurrogate = "au" 27 | 28 | segmentPrefix = "se::" 29 | ) 30 | 31 | const cacheSize = 1000000 32 | 33 | // MakeSurrogateForSegmentChanges creates a surrogate key for the segment being queried 34 | func MakeSurrogateForSegmentChanges(segmentName string) string { 35 | return segmentPrefix + segmentName 36 | } 37 | 38 | // MakeSurrogateForMySegments creates a list surrogate keys for all the segments involved 39 | func MakeSurrogateForMySegments(mysegments []dtos.MySegmentDTO) []string { 40 | // Since we are now evicting individually for every updated key, we don't need surrogates for mySegments 41 | return nil 42 | } 43 | 44 | // MakeMySegmentsEntry create a cache entry key for mysegments 45 | func MakeMySegmentsEntries(key string) []string { 46 | return []string{ 47 | "/api/mySegments/" + key, 48 | "gzip::/api/mySegments/" + key, 49 | } 50 | } 51 | 52 | // MakeProxyCache creates and configures a split-proxy-ready cache 53 | func MakeProxyCache() *gincache.Middleware { 54 | return gincache.New(&gincache.Options{ 55 | SuccessfulOnly: true, // we're not interested in caching non-200 responses 56 | Size: cacheSize, 57 | KeyFactory: keyFactoryFN, 58 | // we make each request handler responsible for generating the surrogates. 59 | // this way we can use segment names as surrogates for mysegments & segment changes 60 | // with a lot less work 61 | SurrogateFactory: func(ctx *gin.Context) []string { return ctx.GetStringSlice(SurrogateContextKey) }, 62 | }) 63 | } 64 | 65 | func keyFactoryFN(ctx *gin.Context) string { 66 | var encodingPrefix string 67 | if strings.Contains(ctx.Request.Header.Get("Accept-Encoding"), "gzip") { 68 | encodingPrefix = "gzip::" 69 | } 70 | 71 | if strings.HasPrefix(ctx.Request.URL.Path, "/api/auth") || strings.HasPrefix(ctx.Request.URL.Path, "/api/v2/auth") { 72 | // For auth requests, since we don't support streaming yet, we only need a single entry in the table, 73 | // so we strip the query-string which contains the user-list 74 | return encodingPrefix + ctx.Request.URL.Path 75 | } 76 | return encodingPrefix + ctx.Request.URL.Path + ctx.Request.URL.RawQuery 77 | } 78 | -------------------------------------------------------------------------------- /splitio/proxy/caching/caching_test.go: -------------------------------------------------------------------------------- 1 | package caching 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/splitio/go-split-commons/v6/dtos" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestCacheKeysDoNotOverlap(t *testing.T) { 14 | 15 | url1, _ := url.Parse("http://proxy.split.io/api/spitChanges?since=-1") 16 | c1 := &gin.Context{Request: &http.Request{URL: url1}} 17 | 18 | url2, _ := url.Parse("http://proxy.split.io/api/spitChanges?s=1.1&since=-1") 19 | c2 := &gin.Context{Request: &http.Request{URL: url2}} 20 | 21 | assert.NotEqual(t, keyFactoryFN(c1), keyFactoryFN(c2)) 22 | } 23 | 24 | func TestSegmentSurrogates(t *testing.T) { 25 | assert.Equal(t, segmentPrefix+"segment1", MakeSurrogateForSegmentChanges("segment1")) 26 | assert.NotEqual(t, MakeSurrogateForSegmentChanges("segment1"), MakeSurrogateForSegmentChanges("segment2")) 27 | } 28 | 29 | func TestMySegmentKeyGeneration(t *testing.T) { 30 | entries := MakeMySegmentsEntries("k1") 31 | assert.Equal(t, "/api/mySegments/k1", entries[0]) 32 | assert.Equal(t, "gzip::/api/mySegments/k1", entries[1]) 33 | } 34 | 35 | func TestMySegmentsSurrogates(t *testing.T) { 36 | assert.Equal(t, []string(nil), MakeSurrogateForMySegments([]dtos.MySegmentDTO{{Name: "segment1"}, {Name: "segment2"}})) 37 | } 38 | -------------------------------------------------------------------------------- /splitio/proxy/controllers/auth.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // AuthServerController bundles all request handler for sdk-server apis 10 | type AuthServerController struct{} 11 | 12 | // NewAuthServerController instantiates a new sdk server controller 13 | func NewAuthServerController() *AuthServerController { 14 | return &AuthServerController{} 15 | } 16 | 17 | // Register mounts the sdk-server endpoints onto the supplied router 18 | func (c *AuthServerController) Register(router gin.IRouter) { 19 | router.GET("/auth", c.AuthV1) 20 | router.GET("/v2/auth", c.AuthV1) 21 | } 22 | 23 | // AuthV1 always returns pushEnabled = false and no token 24 | func (c *AuthServerController) AuthV1(ctx *gin.Context) { 25 | ctx.JSON(http.StatusOK, gin.H{"pushEnabled": false, "token": ""}) 26 | } 27 | -------------------------------------------------------------------------------- /splitio/proxy/controllers/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // APIKeyValidator is a small component that validates apikeys 10 | type APIKeyValidator struct { 11 | apikeys map[string]struct{} 12 | } 13 | 14 | // NewAPIKeyValidator instantiates an apikey validation component 15 | func NewAPIKeyValidator(apikeys []string) *APIKeyValidator { 16 | toRet := &APIKeyValidator{apikeys: make(map[string]struct{})} 17 | 18 | for _, key := range apikeys { 19 | toRet.apikeys[key] = struct{}{} 20 | } 21 | 22 | return toRet 23 | } 24 | 25 | // IsValid checks if an apikey is valid 26 | func (v *APIKeyValidator) IsValid(apikey string) bool { 27 | _, ok := v.apikeys[apikey] 28 | return ok 29 | } 30 | 31 | // AsMiddleware is a function to be used as a gin middleware 32 | func (v *APIKeyValidator) AsMiddleware(ctx *gin.Context) { 33 | auth := strings.Split(ctx.Request.Header.Get("Authorization"), " ") 34 | if len(auth) != 2 || auth[0] != "Bearer" || !v.IsValid(auth[1]) { 35 | ctx.AbortWithStatus(401) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /splitio/proxy/controllers/middleware/auth_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func TestAuthMiddleWare(t *testing.T) { 12 | gin.SetMode(gin.TestMode) 13 | resp := httptest.NewRecorder() 14 | ctx, router := gin.CreateTestContext(resp) 15 | authMW := NewAPIKeyValidator([]string{"apikey1", "apikey2"}) 16 | 17 | router.GET("/api/test", authMW.AsMiddleware, func(ctx *gin.Context) {}) 18 | 19 | ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/test", nil) 20 | ctx.Request.Header.Set("Authorization", "Bearer apikey1") 21 | router.ServeHTTP(resp, ctx.Request) 22 | if resp.Code != 200 { 23 | t.Error("Status code should be 200 and is ", resp.Code) 24 | } 25 | 26 | resp = httptest.NewRecorder() 27 | ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/test", nil) 28 | ctx.Request.Header.Set("Authorization", "Bearer apikey2") 29 | router.ServeHTTP(resp, ctx.Request) 30 | if resp.Code != 200 { 31 | t.Error("Status code should be 200 and is ", resp.Code) 32 | } 33 | 34 | resp = httptest.NewRecorder() 35 | ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/test", nil) 36 | ctx.Request.Header.Set("Authorization", "Bearer apikey3") 37 | router.ServeHTTP(resp, ctx.Request) 38 | if resp.Code != 401 { 39 | t.Error("Status code should be 401 and is ", resp.Code) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /splitio/proxy/controllers/middleware/endpoint.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage" 8 | ) 9 | 10 | // EndpointKey is used to set the endpoint for latency tracker within the request handler 11 | const EndpointKey = "ep" 12 | 13 | // Endpoint paths 14 | const ( 15 | pathSplitChanges = "/api/splitChanges" 16 | pathSegmentChanges = "/api/segmentChanges" 17 | pathMySegments = "/api/mySegments" 18 | pathImpressionsBulk = "/api/testImpressions/bulk" 19 | pathImpressionsCount = "/api/testImpressions/count" 20 | pathImpressionsBulkBeacon = "/api/testImpressions/beacon" 21 | pathImpressionsCountBeacon = "/api/testImpressions/count/beacon" 22 | pathEventsBulk = "/api/events/bulk" 23 | pathEventsBeacon = "/api/events/beacon" 24 | pathTelemetryConfig = "/api/metrics/config" 25 | pathTelemetryUsage = "/api/metrics/usage" 26 | pathTelemetryUsageBeacon = "/api/metrics/usage/beacon" 27 | pathTelemetryUsageBeaconV1 = "/api/v1/metrics/usage/beacon" 28 | pathAuth = "/api/auth" 29 | pathAuthV2 = "/api/auth/v2" 30 | pathTelemetryKeysClientSide = "/api/keys/cs" 31 | pathTelemetryKeysClientSideV1 = "/api/v1/keys/cs" 32 | pathTelemetryKeysClientSideBeacon = "/api/keys/cs/beacon" 33 | pathTelemetryKeysClientSideBeaconV1 = "/api/v1/keys/cs/beacon" 34 | pathTelemetryKeysServerSide = "/api/keys/ss" 35 | pathTelemetryKeysServerSideV1 = "/api/v1/keys/ss" 36 | ) 37 | 38 | // SetEndpoint stores the endpoint in the context for future middleware querying 39 | func SetEndpoint(ctx *gin.Context) { 40 | switch path := ctx.Request.URL.Path; path { 41 | case pathSplitChanges: 42 | ctx.Set(EndpointKey, storage.SplitChangesEndpoint) 43 | case pathImpressionsBulk: 44 | ctx.Set(EndpointKey, storage.ImpressionsBulkEndpoint) 45 | case pathImpressionsCount: 46 | ctx.Set(EndpointKey, storage.ImpressionsCountEndpoint) 47 | case pathImpressionsBulkBeacon: 48 | ctx.Set(EndpointKey, storage.ImpressionsBulkBeaconEndpoint) 49 | case pathImpressionsCountBeacon: 50 | ctx.Set(EndpointKey, storage.ImpressionsCountBeaconEndpoint) 51 | case pathEventsBulk: 52 | ctx.Set(EndpointKey, storage.EventsBulkEndpoint) 53 | case pathEventsBeacon: 54 | ctx.Set(EndpointKey, storage.EventsBulkBeaconEndpoint) 55 | case pathTelemetryConfig: 56 | ctx.Set(EndpointKey, storage.TelemetryConfigEndpoint) 57 | case pathTelemetryUsage: 58 | ctx.Set(EndpointKey, storage.TelemetryRuntimeEndpoint) 59 | case pathTelemetryUsageBeacon, pathTelemetryUsageBeaconV1: 60 | ctx.Set(EndpointKey, storage.TelemetryRuntimeBeaconEndpoint) 61 | case pathAuth, pathAuthV2: 62 | ctx.Set(EndpointKey, storage.AuthEndpoint) 63 | case pathTelemetryKeysClientSide, pathTelemetryKeysClientSideV1: 64 | ctx.Set(EndpointKey, storage.TelemetryKeysClientSideEndpoint) 65 | case pathTelemetryKeysClientSideBeacon, pathTelemetryKeysClientSideBeaconV1: 66 | ctx.Set(EndpointKey, storage.TelemetryKeysClientSideBeaconEndpoint) 67 | case pathTelemetryKeysServerSide, pathTelemetryKeysServerSideV1: 68 | ctx.Set(EndpointKey, storage.TelemetryKeysServerSideEndpoint) 69 | default: 70 | if strings.HasPrefix(path, pathSplitChanges) { 71 | ctx.Set(EndpointKey, storage.SplitChangesEndpoint) 72 | } else if strings.HasPrefix(path, pathSegmentChanges) { 73 | ctx.Set(EndpointKey, storage.SegmentChangesEndpoint) 74 | } else if strings.HasPrefix(path, pathMySegments) { 75 | ctx.Set(EndpointKey, storage.MySegmentsEndpoint) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /splitio/proxy/controllers/middleware/latency.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // MetricsMiddleware is meant to be used for capturing endpoint latencies and return status codes 12 | type MetricsMiddleware struct { 13 | tracker storage.ProxyEndpointTelemetry 14 | } 15 | 16 | // NewProxyMetricsMiddleware instantiates a new local-telemetry tracking middleware 17 | func NewProxyMetricsMiddleware(lats storage.ProxyEndpointTelemetry) *MetricsMiddleware { 18 | return &MetricsMiddleware{tracker: lats} 19 | } 20 | 21 | // Track is the function to be invoked for every request being handled 22 | func (m *MetricsMiddleware) Track(ctx *gin.Context) { 23 | before := time.Now() 24 | ctx.Next() 25 | endpoint, exists := ctx.Get(EndpointKey) 26 | if asInt, ok := endpoint.(int); exists && ok { 27 | m.tracker.RecordEndpointLatency(asInt, time.Now().Sub(before)) 28 | m.tracker.IncrEndpointStatus(asInt, ctx.Writer.Status()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /splitio/proxy/controllers/middleware/latency_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage" 10 | ) 11 | 12 | func TestLatencyMiddleWare(t *testing.T) { 13 | gin.SetMode(gin.TestMode) 14 | resp := httptest.NewRecorder() 15 | ctx, router := gin.CreateTestContext(resp) 16 | 17 | tStorage := storage.NewProxyTelemetryFacade() 18 | tMw := NewProxyMetricsMiddleware(tStorage) 19 | 20 | router.GET("/api/test", tMw.Track, func(ctx *gin.Context) { ctx.Set(EndpointKey, storage.ImpressionsBulkEndpoint) }) 21 | 22 | ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/test", nil) 23 | router.ServeHTTP(resp, ctx.Request) 24 | if resp.Code != 200 { 25 | t.Error("Status code should be 200 and is ", resp.Code) 26 | } 27 | 28 | occurrences := int64(0) 29 | for _, i := range tStorage.PeekEndpointLatency(storage.ImpressionsBulkEndpoint) { 30 | occurrences += i 31 | } 32 | if occurrences != 1 { 33 | t.Error("there should be one latency recorded for impressions bulk posting") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /splitio/proxy/controllers/util.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/splitio/go-split-commons/v6/conf" 6 | "github.com/splitio/go-split-commons/v6/dtos" 7 | ) 8 | 9 | func metadataFromHeaders(ctx *gin.Context) dtos.Metadata { 10 | return dtos.Metadata{ 11 | SDKVersion: ctx.Request.Header.Get("SplitSDKVersion"), 12 | MachineIP: ctx.Request.Header.Get("SplitSDKMachineIP"), 13 | MachineName: ctx.Request.Header.Get("SplitSDKMachineName"), 14 | } 15 | } 16 | 17 | func parseImpressionsMode(mode string) string { 18 | if mode == conf.ImpressionsModeOptimized { 19 | return mode 20 | } 21 | return conf.ImpressionsModeDebug 22 | } 23 | -------------------------------------------------------------------------------- /splitio/proxy/doc.go: -------------------------------------------------------------------------------- 1 | // Package proxy implements proxy service and controllers 2 | package proxy 3 | -------------------------------------------------------------------------------- /splitio/proxy/flagsets/flagsets.go: -------------------------------------------------------------------------------- 1 | package flagsets 2 | 3 | import "golang.org/x/exp/slices" 4 | 5 | type FlagSetMatcher struct { 6 | strict bool 7 | sets map[string]struct{} 8 | } 9 | 10 | func NewMatcher(strict bool, fetched []string) FlagSetMatcher { 11 | out := FlagSetMatcher{ 12 | strict: strict, 13 | sets: make(map[string]struct{}, len(fetched)), 14 | } 15 | 16 | for idx := range fetched { 17 | out.sets[fetched[idx]] = struct{}{} 18 | } 19 | 20 | return out 21 | } 22 | 23 | // Sort, Dedupe & Filter input flagsets. returns sanitized list and a boolean indicating whether a sort was necessary 24 | func (f *FlagSetMatcher) Sanitize(input []string) []string { 25 | if len(input) == 0 { 26 | return input 27 | } 28 | 29 | seen := map[string]struct{}{} 30 | for idx := 0; idx < len(input); idx++ { // cant use range because we're srhinking the slice inside the loop 31 | item := input[idx] 32 | if (f.strict && !setContains(f.sets, item)) || setContains(seen, item) { 33 | if idx+1 < len(input) { 34 | input[idx] = input[len(input)-1] 35 | } 36 | input = input[:len(input)-1] 37 | } 38 | seen[item] = struct{}{} 39 | } 40 | 41 | slices.Sort(input) 42 | return input 43 | } 44 | 45 | func setContains(set map[string]struct{}, item string) bool { 46 | _, ok := set[item] 47 | return ok 48 | } 49 | -------------------------------------------------------------------------------- /splitio/proxy/flagsets/flagsets_test.go: -------------------------------------------------------------------------------- 1 | package flagsets 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFlagSetsMatcher(t *testing.T) { 10 | 11 | m := NewMatcher(false, []string{"s1", "s2", "s3"}) 12 | assert.Equal(t, []string{"s1", "s2", "s3"}, m.Sanitize([]string{"s1", "s2", "s3"})) 13 | assert.Equal(t, []string{"s1", "s2"}, m.Sanitize([]string{"s1", "s2"})) 14 | assert.Equal(t, []string{"s4"}, m.Sanitize([]string{"s4"})) 15 | 16 | m = NewMatcher(true, []string{"s1", "s2", "s3"}) 17 | assert.Equal(t, []string{"s1", "s2", "s3"}, m.Sanitize([]string{"s1", "s2", "s3"})) 18 | assert.Equal(t, []string{"s1", "s2"}, m.Sanitize([]string{"s1", "s2"})) 19 | assert.Equal(t, []string{"s1", "s2"}, m.Sanitize([]string{"s1", "s2", "s7"})) 20 | assert.Equal(t, []string{}, m.Sanitize([]string{"s4"})) 21 | } 22 | -------------------------------------------------------------------------------- /splitio/proxy/initialization_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "sync/atomic" 5 | "testing" 6 | "time" 7 | 8 | "github.com/splitio/go-split-commons/v6/synchronizer" 9 | ) 10 | 11 | type syncManagerMock struct { 12 | c chan int 13 | execCount int64 14 | } 15 | 16 | func (m *syncManagerMock) IsRunning() bool { panic("unimplemented") } 17 | func (m *syncManagerMock) Start() { 18 | atomic.AddInt64(&m.execCount, 1) 19 | switch atomic.LoadInt64(&m.execCount) { 20 | case 1: 21 | m.c <- synchronizer.Error 22 | default: 23 | m.c <- synchronizer.Ready 24 | } 25 | } 26 | func (m *syncManagerMock) Stop() { panic("unimplemented") } 27 | 28 | var _ synchronizer.Manager = (*syncManagerMock)(nil) 29 | 30 | func TestSyncManagerInitializationRetriesWithSnapshot(t *testing.T) { 31 | 32 | sm := &syncManagerMock{c: make(chan int, 1)} 33 | 34 | // No snapshot and error 35 | complete := make(chan struct{}, 1) 36 | err := startBGSyng(sm, sm.c, false, func() { complete <- struct{}{} }) 37 | if err != errUnrecoverable { 38 | t.Error("should be an unrecoverable error. Got: ", err) 39 | } 40 | 41 | select { 42 | case <-complete: 43 | t.Error("nothing should be published on the channel") 44 | case <-time.After(500 * time.Millisecond): 45 | // all good 46 | } 47 | 48 | // Snapshot and error 49 | atomic.StoreInt64(&sm.execCount, 0) 50 | err = startBGSyng(sm, sm.c, true, func() { complete <- struct{}{} }) 51 | if err != errRetrying { 52 | t.Error("should be a retrying error. Got: ", err) 53 | } 54 | 55 | select { 56 | case <-complete: 57 | // all good 58 | case <-time.After(2500 * time.Millisecond): 59 | t.Error("should not time out") 60 | } 61 | 62 | if atomic.LoadInt64(&sm.execCount) != 2 { 63 | t.Error("there should be 2 executions") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /splitio/proxy/internal/dtos.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/splitio/go-split-commons/v6/dtos" 5 | ) 6 | 7 | // RawData represents the raw data submitted by an sdk when posting data with associated metadata 8 | type RawData struct { 9 | Metadata dtos.Metadata 10 | Payload []byte 11 | } 12 | 13 | func newRawData(metadata dtos.Metadata, payload []byte) *RawData { 14 | return &RawEvents{ 15 | Metadata: metadata, 16 | Payload: payload, 17 | } 18 | } 19 | 20 | // RawImpressions represents a raw impression's bulk with associated impressions mode 21 | type RawImpressions struct { 22 | RawData 23 | Mode string 24 | } 25 | 26 | // RawEvents represents the raw data submitted by an sdk when posting impressions 27 | type RawEvents = RawData 28 | 29 | // RawTelemetryConfig represents the raw data submitted by an sdk when posting sdk config 30 | type RawTelemetryConfig = RawData 31 | 32 | // RawTelemetryUsage represents the raw data submitted by an sdk when posting usage metrics 33 | type RawTelemetryUsage = RawData 34 | 35 | // RawImpressionCount represents the raw data submitted by an sdk when posting impression counts 36 | type RawImpressionCount = RawData 37 | 38 | // RawKeysClientSide represents the raw data submitted by an sdk when posting mtks for client side 39 | type RawKeysClientSide = RawData 40 | 41 | // RawKeysServerSide represents the raw data submitted by an sdk when posting mtks for server side 42 | type RawKeysServerSide = RawData 43 | 44 | // NewRawImpressions constructs a RawImpressions wrapper object 45 | func NewRawImpressions(metadata dtos.Metadata, mode string, payload []byte) *RawImpressions { 46 | return &RawImpressions{ 47 | RawData: RawData{ 48 | Metadata: metadata, 49 | Payload: payload, 50 | }, 51 | Mode: mode, 52 | } 53 | } 54 | 55 | // NewRawImpressionCounts constructs a RawImpressionCount wrapper object 56 | func NewRawImpressionCounts(metadata dtos.Metadata, payload []byte) *RawImpressionCount { 57 | return newRawData(metadata, payload) 58 | } 59 | 60 | // NewRawEvents constructs a RawEvents wrapper object 61 | func NewRawEvents(metadata dtos.Metadata, payload []byte) *RawEvents { 62 | return newRawData(metadata, payload) 63 | } 64 | 65 | // NewRawTelemetryConfig constructs a RawEvents wrapper object 66 | func NewRawTelemetryConfig(metadata dtos.Metadata, payload []byte) *RawTelemetryConfig { 67 | return newRawData(metadata, payload) 68 | } 69 | 70 | // NewRawTelemetryUsage constructs a RawEvents wrapper object 71 | func NewRawTelemetryUsage(metadata dtos.Metadata, payload []byte) *RawTelemetryUsage { 72 | return newRawData(metadata, payload) 73 | } 74 | 75 | // NewRawTelemetryKeysClientSide constructs a RawEvents wrapper object 76 | func NewRawTelemetryKeysClientSide(metadata dtos.Metadata, payload []byte) *RawKeysClientSide { 77 | return newRawData(metadata, payload) 78 | } 79 | 80 | // NewRawTelemetryKeysServerSide constructs a RawEvents wrapper object 81 | func NewRawTelemetryKeysServerSide(metadata dtos.Metadata, payload []byte) *RawKeysServerSide { 82 | return newRawData(metadata, payload) 83 | } 84 | -------------------------------------------------------------------------------- /splitio/proxy/storage/mocks/mocks.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/splitio/go-split-commons/v6/dtos" 5 | "github.com/stretchr/testify/mock" 6 | ) 7 | 8 | type ProxySplitStorageMock struct { 9 | mock.Mock 10 | } 11 | 12 | func (p *ProxySplitStorageMock) ChangesSince(since int64, sets []string) (*dtos.SplitChangesDTO, error) { 13 | args := p.Called(since, sets) 14 | return args.Get(0).(*dtos.SplitChangesDTO), args.Error(1) 15 | } 16 | 17 | func (p *ProxySplitStorageMock) RegisterOlderCn(payload *dtos.SplitChangesDTO) { 18 | p.Called(payload) 19 | } 20 | 21 | type ProxySegmentStorageMock struct { 22 | mock.Mock 23 | } 24 | 25 | func (p *ProxySegmentStorageMock) ChangesSince(name string, since int64) (*dtos.SegmentChangesDTO, error) { 26 | args := p.Called(name, since) 27 | return args.Get(0).(*dtos.SegmentChangesDTO), args.Error(1) 28 | } 29 | 30 | func (p *ProxySegmentStorageMock) SegmentsFor(key string) ([]string, error) { 31 | args := p.Called(key) 32 | return args.Get(0).([]string), args.Error(1) 33 | } 34 | 35 | func (p *ProxySegmentStorageMock) CountRemovedKeys(segmentName string) int { 36 | return p.Called(segmentName).Int(0) 37 | } 38 | 39 | type ProxyLargeSegmentStorageMock struct { 40 | mock.Mock 41 | } 42 | 43 | func (s *ProxyLargeSegmentStorageMock) SetChangeNumber(name string, till int64) { 44 | s.Called(name, till).Error(0) 45 | } 46 | 47 | func (s *ProxyLargeSegmentStorageMock) Update(name string, userKeys []string, till int64) { 48 | s.Called(name, userKeys, till) 49 | } 50 | func (s *ProxyLargeSegmentStorageMock) ChangeNumber(name string) int64 { 51 | args := s.Called(name) 52 | return args.Get(0).(int64) 53 | } 54 | func (s *ProxyLargeSegmentStorageMock) Count() int { 55 | args := s.Called() 56 | return args.Get(0).(int) 57 | } 58 | func (s *ProxyLargeSegmentStorageMock) LargeSegmentsForUser(userKey string) []string { 59 | args := s.Called(userKey) 60 | return args.Get(0).([]string) 61 | } 62 | func (s *ProxyLargeSegmentStorageMock) IsInLargeSegment(name string, key string) (bool, error) { 63 | args := s.Called(name, key) 64 | return args.Get(0).(bool), args.Error(1) 65 | } 66 | func (s *ProxyLargeSegmentStorageMock) TotalKeys(name string) int { 67 | return s.Called(name).Get(0).(int) 68 | } 69 | -------------------------------------------------------------------------------- /splitio/proxy/storage/optimized/mocks/mocks.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/splitio/go-split-commons/v6/dtos" 5 | "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage/optimized" 6 | "github.com/stretchr/testify/mock" 7 | ) 8 | 9 | type HistoricStorageMock struct { 10 | mock.Mock 11 | } 12 | 13 | // GetUpdatedSince implements optimized.HistoricChanges 14 | func (h *HistoricStorageMock) GetUpdatedSince(since int64, flagSets []string) []optimized.FeatureView { 15 | return h.Called(since, flagSets).Get(0).([]optimized.FeatureView) 16 | } 17 | 18 | // Update implements optimized.HistoricChanges 19 | func (h *HistoricStorageMock) Update(toAdd []dtos.SplitDTO, toRemove []dtos.SplitDTO, newCN int64) { 20 | h.Called(toAdd, toRemove, newCN) 21 | } 22 | 23 | var _ optimized.HistoricChanges = (*HistoricStorageMock)(nil) 24 | -------------------------------------------------------------------------------- /splitio/proxy/storage/optimized/mysegments.go: -------------------------------------------------------------------------------- 1 | package optimized 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/splitio/go-toolkit/v5/datastructures/set" 9 | ) 10 | 11 | // MySegmentsCache defines the interface for a per-user optimized segment storage 12 | type MySegmentsCache interface { 13 | Update(name string, toAdd *set.ThreadUnsafeSet, toRemove *set.ThreadUnsafeSet) error 14 | SegmentsForUser(key string) []string 15 | KeyCount() int 16 | } 17 | 18 | // MySegmentsCacheImpl implements the MySegmentsCache interface 19 | type MySegmentsCacheImpl struct { 20 | mySegments map[string][]string 21 | mutex *sync.RWMutex 22 | } 23 | 24 | // NewMySegmentsCache constructs a new MySegments cache 25 | func NewMySegmentsCache() *MySegmentsCacheImpl { 26 | return &MySegmentsCacheImpl{ 27 | mySegments: make(map[string][]string), 28 | mutex: &sync.RWMutex{}, 29 | } 30 | } 31 | 32 | // KeyCount retuns the amount of keys who belong to at least one segment 33 | func (m *MySegmentsCacheImpl) KeyCount() int { 34 | m.mutex.RLock() 35 | defer m.mutex.RUnlock() 36 | return len(m.mySegments) 37 | } 38 | 39 | // SegmentsForUser returns the list of segments a certain user belongs to 40 | func (m *MySegmentsCacheImpl) SegmentsForUser(key string) []string { 41 | m.mutex.RLock() 42 | defer m.mutex.RUnlock() 43 | userSegments, ok := m.mySegments[key] 44 | if !ok { 45 | return []string{} 46 | } 47 | return userSegments 48 | } 49 | 50 | // Update adds and removes segments to keys 51 | func (m *MySegmentsCacheImpl) Update(name string, toAdd *set.ThreadUnsafeSet, toRemove *set.ThreadUnsafeSet) error { 52 | m.mutex.Lock() 53 | defer m.mutex.Unlock() 54 | 55 | invalidAdded := []string{} 56 | invalidRemoved := []string{} 57 | for _, addedKey := range toAdd.List() { 58 | strKey, ok := addedKey.(string) 59 | if !ok { 60 | invalidAdded = append(invalidAdded, fmt.Sprintf("%T::%+v", addedKey, addedKey)) 61 | continue 62 | } 63 | m.addSegmentToUser(strKey, name) 64 | } 65 | 66 | for _, removedKey := range toRemove.List() { 67 | strKey, ok := removedKey.(string) 68 | if !ok { 69 | invalidRemoved = append(invalidRemoved, fmt.Sprintf("%T::%+v", removedKey, removedKey)) 70 | continue 71 | } 72 | m.removeSegmentForUser(strKey, name) 73 | } 74 | 75 | if len(invalidAdded) > 0 || len(invalidRemoved) > 0 { 76 | return fmt.Errorf("invalid added and removed keys found: %s // %s", 77 | strings.Join(invalidAdded, ","), strings.Join(invalidRemoved, ",")) 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func (m *MySegmentsCacheImpl) addSegmentToUser(key string, segment string) { 84 | toAdd := []string{segment} 85 | userSegments, ok := m.mySegments[key] 86 | if ok { 87 | if m.isInSegment(segment, userSegments) { 88 | return 89 | } 90 | toAdd = append(userSegments, toAdd...) 91 | } 92 | m.mySegments[key] = toAdd 93 | } 94 | 95 | func (m *MySegmentsCacheImpl) removeSegmentForUser(key string, segment string) { 96 | userSegments, ok := m.mySegments[key] 97 | if !ok { 98 | return 99 | } 100 | toUpdate := make([]string, 0) 101 | for _, s := range userSegments { 102 | if s != segment { 103 | toUpdate = append(toUpdate, s) 104 | } 105 | } 106 | if len(toUpdate) == 0 { 107 | delete(m.mySegments, key) 108 | return 109 | } 110 | m.mySegments[key] = toUpdate 111 | } 112 | 113 | func (m *MySegmentsCacheImpl) isInSegment(segment string, segments []string) bool { 114 | for _, s := range segments { 115 | if s == segment { 116 | return true 117 | } 118 | } 119 | return false 120 | } 121 | -------------------------------------------------------------------------------- /splitio/proxy/storage/optimized/mysegments_test.go: -------------------------------------------------------------------------------- 1 | package optimized 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splitio/go-toolkit/v5/datastructures/set" 7 | ) 8 | 9 | func TestMySegmentsV2(t *testing.T) { 10 | storage := NewMySegmentsCache() 11 | 12 | storage.Update("one", set.NewSet("test"), set.NewSet()) 13 | storage.Update("two", set.NewSet("test"), set.NewSet()) 14 | storage.Update("three", set.NewSet("test"), set.NewSet()) 15 | storage.Update("three", set.NewSet("test"), set.NewSet()) 16 | 17 | segments := storage.SegmentsForUser("test") 18 | if len(segments) != 3 { 19 | t.Error("It should have 3 segments") 20 | } 21 | 22 | if len(storage.SegmentsForUser("nonexistent")) != 0 { 23 | t.Error("It should be empty") 24 | } 25 | 26 | storage.Update("two", set.NewSet(), set.NewSet("test")) 27 | segments = storage.SegmentsForUser("test") 28 | if len(segments) != 2 { 29 | t.Error("It should have 2 segments") 30 | } 31 | 32 | storage.Update("three", set.NewSet(), set.NewSet("test")) 33 | segments = storage.SegmentsForUser("test") 34 | if len(segments) != 1 { 35 | t.Error("It should have 1 segments") 36 | } 37 | 38 | storage.Update("nonexistent", set.NewSet(), set.NewSet("test")) 39 | segments = storage.SegmentsForUser("test") 40 | if len(segments) != 1 { 41 | t.Error("It should have 1 segments") 42 | } 43 | 44 | storage.Update("one", set.NewSet(), set.NewSet("test")) 45 | segments = storage.SegmentsForUser("test") 46 | if len(segments) != 0 { 47 | t.Error("It should be empty") 48 | } 49 | 50 | storage.Update("one", set.NewSet(), set.NewSet("nonexistent")) 51 | } 52 | -------------------------------------------------------------------------------- /splitio/proxy/storage/persistent/helpers.go: -------------------------------------------------------------------------------- 1 | package persistent 2 | 3 | import ( 4 | "encoding/binary" 5 | ) 6 | 7 | // itob returns an 8-byte big endian representation of v. 8 | func itob(v uint64) []byte { 9 | b := make([]byte, 8) 10 | binary.BigEndian.PutUint64(b, uint64(v)) 11 | return b 12 | } 13 | 14 | // btoi returns an uint64 from its 8-byte big endian representation. 15 | func btoi(b []byte) uint64 { 16 | return binary.BigEndian.Uint64(b) 17 | } 18 | -------------------------------------------------------------------------------- /splitio/proxy/storage/persistent/helpers_test.go: -------------------------------------------------------------------------------- 1 | package persistent 2 | 3 | import ( 4 | "encoding/binary" 5 | "testing" 6 | ) 7 | 8 | func TestItob(t *testing.T) { 9 | if x := binary.BigEndian.Uint64(itob(12345)); x != 12345 { 10 | t.Error("should be 12345. Is: ", x) 11 | } 12 | } 13 | 14 | func TestBtoI(t *testing.T) { 15 | b := make([]byte, 8) 16 | binary.BigEndian.PutUint64(b, uint64(12345)) 17 | if x := btoi(b); x != 12345 { 18 | t.Error("should be 12345. Is: ", x) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /splitio/proxy/storage/persistent/mocks/segment.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/splitio/go-toolkit/v5/datastructures/set" 5 | "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage/persistent" 6 | "github.com/stretchr/testify/mock" 7 | ) 8 | 9 | type SegmentChangesCollectionMock struct { 10 | mock.Mock 11 | } 12 | 13 | func (s *SegmentChangesCollectionMock) Update(name string, toAdd *set.ThreadUnsafeSet, toRemove *set.ThreadUnsafeSet, cn int64) error { 14 | return s.Called(name, toAdd, toRemove, cn).Error(0) 15 | } 16 | 17 | func (s *SegmentChangesCollectionMock) Fetch(name string) (*persistent.SegmentChangesItem, error) { 18 | args := s.Called(name) 19 | return args.Get(0).(*persistent.SegmentChangesItem), args.Error(1) 20 | } 21 | 22 | func (s *SegmentChangesCollectionMock) ChangeNumber(segment string) int64 { 23 | return s.Called(segment).Get(0).(int64) 24 | } 25 | 26 | func (s *SegmentChangesCollectionMock) SetChangeNumber(segment string, cn int64) { 27 | s.Called(segment, cn) 28 | } 29 | -------------------------------------------------------------------------------- /splitio/proxy/storage/persistent/segments_test.go: -------------------------------------------------------------------------------- 1 | package persistent 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splitio/go-toolkit/v5/datastructures/set" 7 | "github.com/splitio/go-toolkit/v5/logging" 8 | ) 9 | 10 | func TestSegmentPersistentStorage(t *testing.T) { 11 | dbw, err := NewBoltWrapper(BoltInMemoryMode, nil) 12 | if err != nil { 13 | t.Error("error creating bolt wrapper: ", err) 14 | } 15 | 16 | logger := logging.NewLogger(nil) 17 | segmentC := NewSegmentChangesCollection(dbw, logger) 18 | segmentC.Update("s1", set.NewSet("k1", "k2"), set.NewSet(), 1) 19 | forS1, err := segmentC.Fetch("s1") 20 | if err != nil { 21 | t.Error("err shoud be nil: ", err) 22 | } 23 | 24 | if forS1.Name != "s1" { 25 | t.Error("name should be `s1`") 26 | } 27 | 28 | if len(forS1.Keys) != 2 { 29 | t.Error("should have 2 keys") 30 | } 31 | 32 | if forS1.Keys["k1"].Removed { 33 | t.Error("k1 should not be removed") 34 | } 35 | 36 | forS2, err := segmentC.Fetch("s2") 37 | if forS2 != nil { 38 | t.Error("s2 should not yet exist.", forS2, err) 39 | } 40 | 41 | segmentC.Update("s1", set.NewSet(), set.NewSet("k1"), 2) 42 | forS1, err = segmentC.Fetch("s1") 43 | if err != nil { 44 | t.Error("err shoud be nil: ", err) 45 | } 46 | 47 | if forS1.Name != "s1" { 48 | t.Error("name should be `s1`") 49 | } 50 | 51 | if len(forS1.Keys) != 2 { 52 | t.Error("should have 2 keys", forS1) 53 | } 54 | 55 | if !forS1.Keys["k1"].Removed { 56 | t.Error("k1 should be removed") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /splitio/proxy/storage/persistent/splits.go: -------------------------------------------------------------------------------- 1 | package persistent 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "encoding/json" 7 | "sync" 8 | 9 | "github.com/splitio/go-split-commons/v6/dtos" 10 | "github.com/splitio/go-toolkit/v5/logging" 11 | ) 12 | 13 | const splitChangesCollectionName = "SPLIT_CHANGES_COLLECTION" 14 | 15 | // SplitChangesItem represents an SplitChanges service response 16 | type SplitChangesItem struct { 17 | ChangeNumber int64 `json:"changeNumber"` 18 | Name string `json:"name"` 19 | Status string `json:"status"` 20 | JSON string 21 | } 22 | 23 | // SplitsChangesItems Sortable list 24 | type SplitsChangesItems []SplitChangesItem 25 | 26 | func (slice SplitsChangesItems) Len() int { 27 | return len(slice) 28 | } 29 | 30 | func (slice SplitsChangesItems) Less(i, j int) bool { 31 | return slice[i].ChangeNumber > slice[j].ChangeNumber 32 | } 33 | 34 | func (slice SplitsChangesItems) Swap(i, j int) { 35 | slice[i], slice[j] = slice[j], slice[i] 36 | } 37 | 38 | //---------------------------------------------------- 39 | 40 | // SplitChangesCollection represents a collection of SplitChangesItem 41 | type SplitChangesCollection struct { 42 | collection CollectionWrapper 43 | changeNumber int64 44 | mutex sync.RWMutex 45 | } 46 | 47 | // NewSplitChangesCollection returns an instance of SplitChangesCollection 48 | func NewSplitChangesCollection(db DBWrapper, logger logging.LoggerInterface) *SplitChangesCollection { 49 | return &SplitChangesCollection{ 50 | collection: &BoltDBCollectionWrapper{db: db, name: splitChangesCollectionName, logger: logger}, 51 | changeNumber: 0, 52 | } 53 | } 54 | 55 | // Update processes a set of feature flag changes items + a changeNumber bump atomically 56 | func (c *SplitChangesCollection) Update(toAdd []dtos.SplitDTO, toRemove []dtos.SplitDTO, cn int64) { 57 | 58 | items := make(SplitsChangesItems, 0, len(toAdd)+len(toRemove)) 59 | process := func(split *dtos.SplitDTO) { 60 | asJSON, err := json.Marshal(split) 61 | if err != nil { 62 | // This should not happen unless the DTO class is broken 63 | return 64 | } 65 | items = append(items, SplitChangesItem{ 66 | ChangeNumber: split.ChangeNumber, 67 | Name: split.Name, 68 | Status: split.Status, 69 | JSON: string(asJSON), 70 | }) 71 | } 72 | 73 | for _, split := range toAdd { 74 | process(&split) 75 | } 76 | 77 | for _, split := range toRemove { 78 | process(&split) 79 | } 80 | 81 | c.mutex.Lock() 82 | defer c.mutex.Unlock() 83 | for idx := range items { 84 | err := c.collection.SaveAs([]byte(items[idx].Name), items[idx]) 85 | if err != nil { 86 | // TODO(mredolatti): log 87 | } 88 | } 89 | c.changeNumber = cn 90 | } 91 | 92 | // FetchAll return a SplitChangesItem 93 | func (c *SplitChangesCollection) FetchAll() ([]dtos.SplitDTO, error) { 94 | c.mutex.RLock() 95 | defer c.mutex.RUnlock() 96 | items, err := c.collection.FetchAll() 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | toReturn := make([]dtos.SplitDTO, 0) 102 | 103 | var decodeBuffer bytes.Buffer 104 | for _, v := range items { 105 | var q SplitChangesItem 106 | // resets buffer data 107 | decodeBuffer.Reset() 108 | decodeBuffer.Write(v) 109 | dec := gob.NewDecoder(&decodeBuffer) 110 | 111 | errq := dec.Decode(&q) 112 | if errq != nil { 113 | c.collection.Logger().Error("decode error:", errq, "|", string(v)) 114 | continue 115 | } 116 | 117 | var parsed dtos.SplitDTO 118 | err := json.Unmarshal([]byte(q.JSON), &parsed) 119 | if err != nil { 120 | c.collection.Logger().Error("error decoding feature flag fetched from db: ", err, "|", q.JSON) 121 | continue 122 | } 123 | toReturn = append(toReturn, parsed) 124 | } 125 | 126 | return toReturn, nil 127 | } 128 | 129 | // ChangeNumber returns changeNumber 130 | func (c *SplitChangesCollection) ChangeNumber() int64 { 131 | c.mutex.RLock() 132 | defer c.mutex.RUnlock() 133 | return c.changeNumber 134 | } 135 | -------------------------------------------------------------------------------- /splitio/proxy/storage/persistent/splits_test.go: -------------------------------------------------------------------------------- 1 | package persistent 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splitio/go-split-commons/v6/dtos" 7 | "github.com/splitio/go-toolkit/v5/logging" 8 | ) 9 | 10 | func TestSplitPersistentStorage(t *testing.T) { 11 | dbw, err := NewBoltWrapper(BoltInMemoryMode, nil) 12 | if err != nil { 13 | t.Error("error creating bolt wrapper: ", err) 14 | } 15 | 16 | logger := logging.NewLogger(nil) 17 | splitC := NewSplitChangesCollection(dbw, logger) 18 | 19 | splitC.Update([]dtos.SplitDTO{ 20 | {Name: "s1", ChangeNumber: 1, Status: "ACTIVE"}, 21 | {Name: "s2", ChangeNumber: 1, Status: "ACTIVE"}, 22 | }, nil, 1) 23 | 24 | all, err := splitC.FetchAll() 25 | if err != nil { 26 | t.Error("FetchAll should not return an error. Got: ", err) 27 | } 28 | 29 | if len(all) != 2 { 30 | t.Error("invalid number of items fetched.") 31 | return 32 | } 33 | 34 | if all[0].Name != "s1" || all[1].Name != "s2" { 35 | t.Error("Invalid payload in fetched changes.") 36 | } 37 | 38 | if splitC.ChangeNumber() != 1 { 39 | t.Error("CN should be 1.") 40 | } 41 | 42 | splitC.Update([]dtos.SplitDTO{{Name: "s1", ChangeNumber: 2, Status: "ARCHIVED"}}, nil, 2) 43 | all, err = splitC.FetchAll() 44 | if err != nil { 45 | t.Error("FetchAll should not return an error. Got: ", err) 46 | } 47 | 48 | if len(all) != 2 { 49 | t.Error("invalid number of items fetched.") 50 | return 51 | } 52 | 53 | if all[0].Name != "s1" || all[0].Status != "ARCHIVED" { 54 | t.Error("s1 should be archived.") 55 | } 56 | 57 | if splitC.ChangeNumber() != 2 { 58 | t.Error("CN should be 2.") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /splitio/proxy/storage/segments_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splitio/go-toolkit/v5/logging" 7 | "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage/optimized" 8 | "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage/persistent" 9 | "github.com/splitio/split-synchronizer/v5/splitio/proxy/storage/persistent/mocks" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestSegmentStorage(t *testing.T) { 14 | 15 | psm := &mocks.SegmentChangesCollectionMock{} 16 | psm.On("Fetch", "some").Return(&persistent.SegmentChangesItem{ 17 | Name: "some", 18 | Keys: map[string]persistent.SegmentKey{ 19 | "k1": {Name: "k1", ChangeNumber: 1, Removed: false}, 20 | "k2": {Name: "k2", ChangeNumber: 1, Removed: true}, 21 | "k3": {Name: "k3", ChangeNumber: 2, Removed: false}, 22 | "k4": {Name: "k4", ChangeNumber: 2, Removed: true}, 23 | "k5": {Name: "k5", ChangeNumber: 3, Removed: false}, 24 | "k6": {Name: "k6", ChangeNumber: 3, Removed: true}, 25 | "k7": {Name: "k7", ChangeNumber: 4, Removed: false}, 26 | }, 27 | }, nil) 28 | 29 | ss := ProxySegmentStorageImpl{ 30 | logger: logging.NewLogger(nil), 31 | db: psm, 32 | mysegments: optimized.NewMySegmentsCache(), 33 | } 34 | 35 | changes, err := ss.ChangesSince("some", -1) 36 | assert.Nil(t, err) 37 | assert.Equal(t, "some", changes.Name) 38 | assert.ElementsMatch(t, []string{"k1", "k3", "k5", "k7"}, changes.Added) 39 | assert.ElementsMatch(t, []string{}, changes.Removed) 40 | assert.Equal(t, int64(-1), changes.Since) 41 | assert.Equal(t, int64(4), changes.Till) 42 | 43 | changes, err = ss.ChangesSince("some", 1) 44 | assert.Nil(t, err) 45 | assert.Equal(t, "some", changes.Name) 46 | assert.ElementsMatch(t, []string{"k3", "k5", "k7"}, changes.Added) 47 | assert.ElementsMatch(t, []string{"k4", "k6"}, changes.Removed) 48 | assert.Equal(t, int64(1), changes.Since) 49 | assert.Equal(t, int64(4), changes.Till) 50 | 51 | changes, err = ss.ChangesSince("some", 2) 52 | assert.Nil(t, err) 53 | assert.Equal(t, "some", changes.Name) 54 | assert.ElementsMatch(t, []string{"k5", "k7"}, changes.Added) 55 | assert.ElementsMatch(t, []string{"k6"}, changes.Removed) 56 | assert.Equal(t, int64(2), changes.Since) 57 | assert.Equal(t, int64(4), changes.Till) 58 | 59 | changes, err = ss.ChangesSince("some", 3) 60 | assert.Nil(t, err) 61 | assert.Equal(t, "some", changes.Name) 62 | assert.ElementsMatch(t, []string{"k7"}, changes.Added) 63 | assert.ElementsMatch(t, []string{}, changes.Removed) 64 | assert.Equal(t, int64(3), changes.Since) 65 | assert.Equal(t, int64(4), changes.Till) 66 | 67 | changes, err = ss.ChangesSince("some", 4) 68 | assert.Nil(t, err) 69 | assert.Equal(t, "some", changes.Name) 70 | assert.ElementsMatch(t, []string{}, changes.Added) 71 | assert.ElementsMatch(t, []string{}, changes.Removed) 72 | assert.Equal(t, int64(4), changes.Since) 73 | assert.Equal(t, int64(4), changes.Till) 74 | 75 | } 76 | -------------------------------------------------------------------------------- /splitio/proxy/tasks/deferred.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | 7 | "github.com/splitio/go-split-commons/v6/tasks" 8 | "github.com/splitio/go-toolkit/v5/asynctask" 9 | "github.com/splitio/go-toolkit/v5/logging" 10 | gtSync "github.com/splitio/go-toolkit/v5/sync" 11 | "github.com/splitio/go-toolkit/v5/workerpool" 12 | ) 13 | 14 | // Right now, proxy mode has impressions, events & telemetry refresh rate properties. It's not really clear whether they add value or not 15 | // to have such behaviour, but in order to not break everything now, we'll try to maintain that behavior. 16 | // In order to do so, we're replacing the prevoius struct with multiple nested maps for 2 channels. 17 | // An explicit one, which will capture incoming data from POST requests and an implicit one managed by a worker pool which will have N goroutines 18 | // blocked there waiting for data to be pushed into so that they can post it to our BE. 19 | // An async task will periodically flush the queue, by moving those elements into the worker pool's one. (we're using pointers 20 | // the cost of copying those structs everywhere). 21 | // The worker pool now defines the level of concurrency when posting data 22 | // The size of the incoming & worker pool channels, define the amount of impression posts that can be kept in memory 23 | 24 | // ErrQueueFull is returned when attempting to add data to a full queue 25 | var ErrQueueFull = errors.New("queue is full, data not pushed") 26 | 27 | // DeferredRecordingTask defines the interface for a task that accepts POSTs and submits them asyncrhonously 28 | type DeferredRecordingTask interface { 29 | Stage(rawData interface{}) error 30 | tasks.Task 31 | } 32 | 33 | // WorkerFactory defines the signature of a function for instantiating workers 34 | type WorkerFactory = func() workerpool.Worker 35 | 36 | type genericQueue = chan interface{} 37 | 38 | // DeferredRecordingTaskImpl is in charge of fetching impressions from the queue and posting them to the Split server BE 39 | type DeferredRecordingTaskImpl struct { 40 | logger logging.LoggerInterface 41 | task *asynctask.AsyncTask 42 | drainInProgress *gtSync.AtomicBool 43 | pool *workerpool.WorkerAdmin 44 | queue genericQueue 45 | mutex sync.Mutex 46 | } 47 | 48 | func newDeferredFlushTask(logger logging.LoggerInterface, wfactory WorkerFactory, period int, queueSize int, threads int) *DeferredRecordingTaskImpl { 49 | drainFlag := gtSync.NewAtomicBool(false) 50 | queue := make(genericQueue, queueSize) 51 | pool := workerpool.NewWorkerAdmin(queueSize, logger) 52 | trigger := func(loger logging.LoggerInterface) error { 53 | if !drainFlag.TestAndSet() { 54 | logger.Warning("Impressions flush requested while another one is in progress. Ignoring.") 55 | return nil 56 | } 57 | defer drainFlag.Unset() // clear the flag after we're done 58 | for len(queue) > 0 { 59 | pool.QueueMessage(<-queue) 60 | } 61 | return nil 62 | } 63 | 64 | for i := 0; i < threads; i++ { 65 | pool.AddWorker(wfactory()) 66 | } 67 | 68 | return &DeferredRecordingTaskImpl{ 69 | logger: logger, 70 | task: asynctask.NewAsyncTask("impressions-recorder", trigger, period, nil, nil, logger), 71 | drainInProgress: drainFlag, 72 | pool: pool, 73 | queue: queue, 74 | } 75 | } 76 | 77 | // Stage queues impressions to be sent when the timer expires or the queue is filled. 78 | func (t *DeferredRecordingTaskImpl) Stage(data interface{}) error { 79 | t.mutex.Lock() 80 | defer t.mutex.Unlock() 81 | select { 82 | case t.queue <- data: 83 | default: 84 | return ErrQueueFull 85 | } 86 | 87 | if len(t.queue) == cap(t.queue) { // The queue has become full with this new element we added 88 | t.task.WakeUp() 89 | } 90 | return nil 91 | } 92 | 93 | // Start starts the flushing task 94 | func (t *DeferredRecordingTaskImpl) Start() { 95 | t.task.Start() 96 | } 97 | 98 | // Stop stops the flushing task 99 | func (t *DeferredRecordingTaskImpl) Stop(blocking bool) error { 100 | return t.task.Stop(blocking) 101 | } 102 | 103 | // IsRunning returns whether the task is running 104 | func (t *DeferredRecordingTaskImpl) IsRunning() bool { 105 | return t.task.IsRunning() 106 | } 107 | 108 | var _ DeferredRecordingTask = (*DeferredRecordingTaskImpl)(nil) 109 | -------------------------------------------------------------------------------- /splitio/proxy/tasks/events.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/splitio/go-split-commons/v6/service/api" 7 | "github.com/splitio/go-toolkit/v5/common" 8 | "github.com/splitio/go-toolkit/v5/logging" 9 | "github.com/splitio/go-toolkit/v5/workerpool" 10 | 11 | "github.com/splitio/split-synchronizer/v5/splitio/proxy/internal" 12 | ) 13 | 14 | // EventWorker defines a component capable of recording imrpessions in raw form 15 | type EventWorker struct { 16 | name string 17 | logger logging.LoggerInterface 18 | recorder *api.HTTPEventsRecorder 19 | } 20 | 21 | // Name returns the name of the worker 22 | func (w *EventWorker) Name() string { return w.name } 23 | 24 | // OnError is called whenever theres an error in the worker function 25 | func (w *EventWorker) OnError(e error) {} 26 | 27 | // Cleanup is called after the worker is shutdown 28 | func (w *EventWorker) Cleanup() error { return nil } 29 | 30 | // FailureTime specifies how long to wait when an errors occurs before executing again 31 | func (w *EventWorker) FailureTime() int64 { return 1 } 32 | 33 | // DoWork is called and passed a message fetched from the work queue 34 | func (w *EventWorker) DoWork(message interface{}) error { 35 | asEvents, ok := message.(*internal.RawEvents) 36 | if !ok { 37 | w.logger.Error(fmt.Sprintf("invalid data fetched from queue. Expected RawEvents. Got '%T'", message)) 38 | return nil 39 | } 40 | 41 | w.recorder.RecordRaw("/events/bulk", asEvents.Payload, asEvents.Metadata, nil) 42 | return nil 43 | } 44 | 45 | func newEventWorkerFactory(name string, recorder *api.HTTPEventsRecorder, logger logging.LoggerInterface) WorkerFactory { 46 | var i *int = common.IntRef(0) 47 | return func() workerpool.Worker { 48 | defer func() { *i++ }() 49 | return &EventWorker{name: fmt.Sprintf("%s_%d", name, i), logger: logger, recorder: recorder} 50 | } 51 | } 52 | 53 | // NewEventsFlushTask creates a new impressions flushing task 54 | func NewEventsFlushTask(recorder *api.HTTPEventsRecorder, logger logging.LoggerInterface, period int, queueSize int, threads int) *DeferredRecordingTaskImpl { 55 | return newDeferredFlushTask(logger, newEventWorkerFactory("events-worker", recorder, logger), period, queueSize, threads) 56 | } 57 | -------------------------------------------------------------------------------- /splitio/proxy/tasks/impcount.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/splitio/go-split-commons/v6/service/api" 7 | "github.com/splitio/go-toolkit/v5/common" 8 | "github.com/splitio/go-toolkit/v5/logging" 9 | "github.com/splitio/go-toolkit/v5/workerpool" 10 | 11 | "github.com/splitio/split-synchronizer/v5/splitio/proxy/internal" 12 | ) 13 | 14 | // ImpressionCountWorker defines a component capable of recording imrpessions in raw form 15 | type ImpressionCountWorker struct { 16 | name string 17 | logger logging.LoggerInterface 18 | recorder *api.HTTPImpressionRecorder 19 | } 20 | 21 | // Name returns the name of the worker 22 | func (w *ImpressionCountWorker) Name() string { return w.name } 23 | 24 | // OnError is called whenever theres an error in the worker function 25 | func (w *ImpressionCountWorker) OnError(e error) {} 26 | 27 | // Cleanup is called after the worker is shutdown 28 | func (w *ImpressionCountWorker) Cleanup() error { return nil } 29 | 30 | // FailureTime specifies how long to wait when an errors occurs before executing again 31 | func (w *ImpressionCountWorker) FailureTime() int64 { return 1 } 32 | 33 | // DoWork is called and passed a message fetched from the work queue 34 | func (w *ImpressionCountWorker) DoWork(message interface{}) error { 35 | asCounts, ok := message.(*internal.RawImpressionCount) 36 | if !ok { 37 | w.logger.Error(fmt.Sprintf("invalid data fetched from queue. Expected RawImpressions. Got '%T'", message)) 38 | return nil 39 | } 40 | 41 | err := w.recorder.RecordRaw("/testImpressions/count", asCounts.Payload, asCounts.Metadata, nil) 42 | if err != nil { 43 | return fmt.Errorf("error posting impression counts to Split servers: %w", err) 44 | } 45 | return nil 46 | } 47 | 48 | func newImpressionCountWorkerFactory( 49 | name string, 50 | recorder *api.HTTPImpressionRecorder, 51 | logger logging.LoggerInterface, 52 | ) WorkerFactory { 53 | var i *int = common.IntRef(0) 54 | return func() workerpool.Worker { 55 | defer func() { *i++ }() 56 | return &ImpressionCountWorker{name: fmt.Sprintf("%s_%d", name, i), logger: logger, recorder: recorder} 57 | } 58 | } 59 | 60 | // NewImpressionCountFlushTask creates a new impressions flushing task 61 | func NewImpressionCountFlushTask( 62 | recorder *api.HTTPImpressionRecorder, 63 | logger logging.LoggerInterface, 64 | period int, 65 | queueSize int, 66 | threads int, 67 | ) *DeferredRecordingTaskImpl { 68 | return newDeferredFlushTask( 69 | logger, 70 | newImpressionCountWorkerFactory("impressions-count-worker", recorder, logger), 71 | period, 72 | queueSize, 73 | threads, 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /splitio/proxy/tasks/impressions.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/splitio/go-split-commons/v6/service/api" 7 | "github.com/splitio/go-toolkit/v5/common" 8 | "github.com/splitio/go-toolkit/v5/logging" 9 | "github.com/splitio/go-toolkit/v5/workerpool" 10 | 11 | "github.com/splitio/split-synchronizer/v5/splitio/proxy/internal" 12 | ) 13 | 14 | // ImpressionWorker defines a component capable of recording imrpessions in raw form 15 | type ImpressionWorker struct { 16 | name string 17 | logger logging.LoggerInterface 18 | recorder *api.HTTPImpressionRecorder 19 | } 20 | 21 | // Name returns the name of the worker 22 | func (w *ImpressionWorker) Name() string { return w.name } 23 | 24 | // OnError is called whenever theres an error in the worker function 25 | func (w *ImpressionWorker) OnError(e error) {} 26 | 27 | // Cleanup is called after the worker is shutdown 28 | func (w *ImpressionWorker) Cleanup() error { return nil } 29 | 30 | // FailureTime specifies how long to wait when an errors occurs before executing again 31 | func (w *ImpressionWorker) FailureTime() int64 { return 1 } 32 | 33 | // DoWork is called and passed a message fetched from the work queue 34 | func (w *ImpressionWorker) DoWork(message interface{}) error { 35 | asImpressions, ok := message.(*internal.RawImpressions) 36 | if !ok { 37 | w.logger.Error(fmt.Sprintf("invalid data fetched from queue. Expected RawImpressions. Got '%T'", message)) 38 | return nil 39 | } 40 | 41 | extraHeaders := map[string]string{"SDKImpressionsMode": asImpressions.Mode} 42 | err := w.recorder.RecordRaw("/testImpressions/bulk", asImpressions.Payload, asImpressions.Metadata, extraHeaders) 43 | 44 | if err != nil { 45 | return fmt.Errorf("error posting impressions to Split servers: %w", err) 46 | } 47 | return nil 48 | } 49 | 50 | func newImpressionWorkerFactory( 51 | name string, 52 | recorder *api.HTTPImpressionRecorder, 53 | logger logging.LoggerInterface, 54 | ) WorkerFactory { 55 | var i *int = common.IntRef(0) 56 | return func() workerpool.Worker { 57 | defer func() { *i++ }() 58 | return &ImpressionWorker{name: fmt.Sprintf("%s_%d", name, i), logger: logger, recorder: recorder} 59 | } 60 | } 61 | 62 | // NewImpressionsFlushTask creates a new impressions flushing task 63 | func NewImpressionsFlushTask( 64 | recorder *api.HTTPImpressionRecorder, 65 | logger logging.LoggerInterface, 66 | period int, 67 | queueSize int, 68 | threads int, 69 | ) *DeferredRecordingTaskImpl { 70 | return newDeferredFlushTask( 71 | logger, 72 | newImpressionWorkerFactory("impressions-worker", recorder, logger), 73 | period, 74 | queueSize, 75 | threads, 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /splitio/proxy/tasks/mocks/deferred.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | type MockDeferredRecordingTask struct { 4 | StageCall func(rawData interface{}) error 5 | StartCall func() 6 | StopCall func(blocking bool) error 7 | IsRunningCall func() bool 8 | } 9 | 10 | func (t *MockDeferredRecordingTask) Stage(rawData interface{}) error { 11 | return t.StageCall(rawData) 12 | } 13 | 14 | func (t *MockDeferredRecordingTask) Start() { 15 | t.StartCall() 16 | } 17 | 18 | func (t *MockDeferredRecordingTask) Stop(blocking bool) error { 19 | return t.StopCall(blocking) 20 | } 21 | 22 | func (t *MockDeferredRecordingTask) IsRunning() bool { 23 | return t.IsRunningCall() 24 | } 25 | -------------------------------------------------------------------------------- /splitio/util/tls.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "strings" 10 | 11 | "github.com/splitio/split-synchronizer/v5/splitio/common/conf" 12 | ) 13 | 14 | var ( 15 | ErrTLSEmptyCertOrPK = errors.New("when TLS is enabled, server certificate chain & server private key parameters are mandatory") 16 | ErrTLSInvalidVersion = errors.New("invalid TLS version") 17 | ) 18 | 19 | func TLSConfigForServer(cfg *conf.TLS) (*tls.Config, error) { 20 | if !cfg.Enabled { 21 | return nil, nil 22 | } 23 | 24 | if cfg.CertChainFN == "" || cfg.PrivateKeyFN == "" { 25 | return nil, ErrTLSEmptyCertOrPK 26 | } 27 | 28 | version, err := parseMinTLSVersion(cfg.MinTLSVersion) 29 | if err != nil { 30 | return nil, fmt.Errorf("error parsing min tls version: %w", err) 31 | } 32 | 33 | cert, err := tls.LoadX509KeyPair(cfg.CertChainFN, cfg.PrivateKeyFN) 34 | if err != nil { 35 | return nil, fmt.Errorf("error loading cert/key pair: %w", err) 36 | } 37 | 38 | tlsConfig := &tls.Config{ 39 | ServerName: cfg.ServerName, 40 | MinVersion: version, 41 | Certificates: []tls.Certificate{cert}, 42 | } 43 | 44 | if len(cfg.AllowedCipherSuites) > 0 { 45 | suites, err := parseCipherSuites(strings.Split(cfg.AllowedCipherSuites, ",")) 46 | if err != nil { 47 | return nil, fmt.Errorf("error parsing cipher suites: %w", err) 48 | } 49 | tlsConfig.CipherSuites = suites 50 | } 51 | 52 | if !cfg.ClientValidation { 53 | return tlsConfig, nil 54 | } 55 | 56 | tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert 57 | if cfg.ClientValidationRootCert != "" { 58 | certBytes, err := ioutil.ReadFile(cfg.ClientValidationRootCert) 59 | if err != nil { 60 | return nil, fmt.Errorf("error reading root certificate for client validation") 61 | } 62 | 63 | certPool := x509.NewCertPool() 64 | certPool.AppendCertsFromPEM(certBytes) 65 | tlsConfig.ClientCAs = certPool 66 | } 67 | 68 | return tlsConfig, nil 69 | } 70 | 71 | func parseMinTLSVersion(version string) (uint16, error) { 72 | switch version { 73 | case "1.0": 74 | return tls.VersionTLS10, nil 75 | case "1.1": 76 | return tls.VersionTLS11, nil 77 | case "1.2": 78 | return tls.VersionTLS12, nil 79 | case "1.3": 80 | return tls.VersionTLS13, nil 81 | } 82 | return 0, ErrTLSInvalidVersion 83 | } 84 | 85 | func parseCipherSuites(strSuites []string) ([]uint16, error) { 86 | valid := tls.CipherSuites() 87 | requested := make([]uint16, 0, len(strSuites)) 88 | for _, suite := range strSuites { 89 | suite = strings.TrimSpace(suite) 90 | found := false 91 | for _, current := range valid { 92 | if current.Name == suite { 93 | requested = append(requested, current.ID) 94 | found = true 95 | break 96 | } 97 | } 98 | if !found { 99 | return nil, fmt.Errorf("cipher suite '%s' not found in list of secure ones", suite) 100 | } 101 | } 102 | return requested, nil 103 | } 104 | -------------------------------------------------------------------------------- /splitio/util/tls_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/tls" 5 | "testing" 6 | 7 | "github.com/splitio/split-synchronizer/v5/splitio/common/conf" 8 | ) 9 | 10 | func TestTLSConfigForServer(t *testing.T) { 11 | res, err := TLSConfigForServer(&conf.TLS{Enabled: false}) 12 | if err != nil { 13 | t.Error("no err should be returned. Got: ", err) 14 | } 15 | 16 | if res != nil { 17 | t.Error("config should be nil if TLS is not enabled. Got: ", res) 18 | } 19 | 20 | res, err = TLSConfigForServer(&conf.TLS{Enabled: true}) 21 | if err != ErrTLSEmptyCertOrPK { 22 | t.Error("should return ErrTLSEmptyCertOrPK. Got: ", err) 23 | } 24 | 25 | if res != nil { 26 | t.Error("config should be nil on error. Got", res) 27 | } 28 | 29 | res, err = TLSConfigForServer(&conf.TLS{ 30 | Enabled: true, 31 | CertChainFN: "nonexistant.crt", 32 | PrivateKeyFN: "nonexistant.pem", 33 | }) 34 | if err == nil { 35 | t.Error("there should be an error with nonexistant files") 36 | } 37 | 38 | if res != nil { 39 | t.Error("config should be nil on error. Got", res) 40 | } 41 | 42 | res, err = TLSConfigForServer(&conf.TLS{ 43 | Enabled: true, 44 | CertChainFN: "../../test/certs/https/proxy.crt", 45 | PrivateKeyFN: "../../test/certs/https/proxy.key", 46 | MinTLSVersion: "1.3", 47 | }) 48 | if err != nil { 49 | t.Error("there should be no error. Got: ", err) 50 | } 51 | 52 | if len(res.Certificates) != 1 { 53 | t.Error("there should be 1 certificate. Have: ", res.Certificates) 54 | } 55 | 56 | if res.ClientAuth != 0 { 57 | t.Error("client auth should be disabled") 58 | } 59 | 60 | res, err = TLSConfigForServer(&conf.TLS{ 61 | Enabled: true, 62 | CertChainFN: "../../test/certs/https/proxy.crt", 63 | PrivateKeyFN: "../../test/certs/https/proxy.key", 64 | MinTLSVersion: "1.3", 65 | ClientValidation: true, 66 | }) 67 | if err != nil { 68 | t.Error("there should be no error. Got: ", err) 69 | } 70 | 71 | if len(res.Certificates) != 1 { 72 | t.Error("there should be 1 certificate. Have: ", res.Certificates) 73 | } 74 | 75 | if res.ClientAuth != tls.RequireAndVerifyClientCert { 76 | t.Error("client auth should be disabled") 77 | } 78 | 79 | if res.ClientCAs != nil { 80 | t.Error("no root CA should be used for client-validation purposes") 81 | } 82 | 83 | res, err = TLSConfigForServer(&conf.TLS{ 84 | Enabled: true, 85 | CertChainFN: "../../test/certs/https/proxy.crt", 86 | PrivateKeyFN: "../../test/certs/https/proxy.key", 87 | MinTLSVersion: "1.3", 88 | ClientValidation: true, 89 | ClientValidationRootCert: "../../test/certs/https/ca.crt", 90 | }) 91 | if err != nil { 92 | t.Error("there should be no error. Got: ", err) 93 | } 94 | 95 | if len(res.Certificates) != 1 { 96 | t.Error("there should be 1 certificate. Have: ", res.Certificates) 97 | } 98 | 99 | if res.ClientAuth != tls.RequireAndVerifyClientCert { 100 | t.Error("client auth should be disabled") 101 | } 102 | 103 | if res.ClientCAs == nil { 104 | t.Error("a root certificate pool should be set for client validation") 105 | } 106 | 107 | res, err = TLSConfigForServer(&conf.TLS{ 108 | Enabled: true, 109 | CertChainFN: "../../test/certs/https/proxy.crt", 110 | PrivateKeyFN: "../../test/certs/https/proxy.key", 111 | MinTLSVersion: "1.3", 112 | AllowedCipherSuites: "TLS_RSA_WITH_RC4_128_SHA", 113 | }) 114 | 115 | if err == nil { 116 | t.Error("should fail for using an insecure cipher suite 'TLS_RSA_WITH_RC4_128_SHA'") 117 | } 118 | 119 | res, err = TLSConfigForServer(&conf.TLS{ 120 | Enabled: true, 121 | CertChainFN: "../../test/certs/https/proxy.crt", 122 | PrivateKeyFN: "../../test/certs/https/proxy.key", 123 | MinTLSVersion: "1.3", 124 | AllowedCipherSuites: "TLS_CHACHA20_POLY1305_SHA256", 125 | }) 126 | 127 | if err != nil { 128 | t.Error("should not fail with a secure cipher suite 'TLS_CHACHA20_POLY1305_SHA256'") 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /splitio/util/utils.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/splitio/go-split-commons/v6/dtos" 9 | "github.com/splitio/go-toolkit/v5/hasher" 10 | "github.com/splitio/go-toolkit/v5/nethelpers" 11 | "github.com/splitio/split-synchronizer/v5/splitio" 12 | ) 13 | 14 | // HashAPIKey hashes apikey 15 | func HashAPIKey(apikey string) uint32 { 16 | murmur32 := hasher.NewMurmur332Hasher(0) 17 | return murmur32.Hash([]byte(apikey)) 18 | } 19 | 20 | // GetClientKey accepts an apikey and extracts the "client-key" portion of it 21 | func GetClientKey(apikey string) (string, error) { 22 | if len(apikey) < 4 { 23 | return "", errors.New("apikey too short") 24 | } 25 | return apikey[len(apikey)-4:], nil 26 | } 27 | 28 | // GetMetadata wrapps metadata 29 | func GetMetadata(proxy bool, ipAddressEnabled bool) dtos.Metadata { 30 | instanceName := "unknown" 31 | ipAddress := "unknown" 32 | if ipAddressEnabled { 33 | ip, err := nethelpers.ExternalIP() 34 | if err == nil { 35 | ipAddress = ip 36 | instanceName = fmt.Sprintf("ip-%s", strings.Replace(ipAddress, ".", "-", -1)) 37 | } 38 | } 39 | 40 | appName := "SplitSyncProducerMode-" 41 | if proxy { 42 | appName = "SplitSyncProxyMode-" 43 | } 44 | 45 | return dtos.Metadata{ 46 | MachineIP: ipAddress, 47 | MachineName: instanceName, 48 | SDKVersion: appName + splitio.Version, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /splitio/util/utils_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bufio" 5 | "encoding/csv" 6 | "io" 7 | "os" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/splitio/go-toolkit/v5/hasher" 12 | ) 13 | 14 | func TestMurmurHashOnAlphanumericData(t *testing.T) { 15 | inFile, _ := os.Open("../../test/murmur/murmur3-sample-data-v2.csv") 16 | defer inFile.Close() 17 | 18 | reader := csv.NewReader(bufio.NewReader(inFile)) 19 | 20 | var arr []string 21 | var err error 22 | line := 0 23 | for err != io.EOF { 24 | line++ 25 | arr, err = reader.Read() 26 | if len(arr) < 4 { 27 | continue // Skip empty lines 28 | } 29 | seed, _ := strconv.ParseInt(arr[0], 10, 32) 30 | str := arr[1] 31 | digest, _ := strconv.ParseUint(arr[2], 10, 32) 32 | 33 | murmur := hasher.NewMurmur332Hasher(uint32(seed)) 34 | calculated := murmur.Hash([]byte(str)) 35 | if calculated != uint32(digest) { 36 | t.Errorf("%d: Murmur hash calculation failed for string %s. Should be %d and was %d", line, str, digest, calculated) 37 | break 38 | } 39 | } 40 | } 41 | 42 | func TestMurmurHashOnNonAlphanumericData(t *testing.T) { 43 | inFile, _ := os.Open("../../test/murmur/murmur3-sample-data-non-alpha-numeric-v2.csv") 44 | defer inFile.Close() 45 | 46 | reader := csv.NewReader(bufio.NewReader(inFile)) 47 | 48 | var arr []string 49 | var err error 50 | line := 0 51 | for err != io.EOF { 52 | line++ 53 | arr, err = reader.Read() 54 | if len(arr) < 4 { 55 | continue // Skip empty lines 56 | } 57 | seed, _ := strconv.ParseInt(arr[0], 10, 32) 58 | str := arr[1] 59 | digest, _ := strconv.ParseUint(arr[2], 10, 32) 60 | 61 | murmur := hasher.NewMurmur332Hasher(uint32(seed)) 62 | calculated := murmur.Hash([]byte(str)) 63 | if calculated != uint32(digest) { 64 | t.Errorf("%d: Murmur hash calculation failed for string %s. Should be %d and was %d", line, str, digest, calculated) 65 | break 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /splitio/version.go: -------------------------------------------------------------------------------- 1 | // Package splitio provides core functionality for interacting with Split Software Services. 2 | package splitio 3 | 4 | // Version is the version of this Agent 5 | const Version = "5.10.2" 6 | -------------------------------------------------------------------------------- /test/certs/client-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDDDCCAfSgAwIBAgIJAIrEAtk5FBq3MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0xOTA3MDEyMDM1MzlaFw0zMzAzMDkyMDM1MzlaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBAMeOZhBXmyTNnDkgIurJpqAjILzg2WEjnyQooRVw3b5nUAMg5e9AcAUJzt2R 6 | 3ME91yl/HZe78pYX0wc1SV8LCM07wqRw1aFUP+FcwoTjRNiMRVXKAGx5M0IZhJgz 7 | dxPqmtcuW47OXEKDDTGEv6Mt9XQzj/6LTOhOx3QsTRSMIyV00QP6AC+9X0+WA1BK 8 | oAbkLN8zgaxXZsfi8pztlSucZWGtdnx0P7fvcXmQu5dyAJdZ6/xe9O2MShWTBx5t 9 | Bm3f1cS35fXfn1EEWKl98j9fnmYpplVqjrUnVXJxY+XEXXulLiRS1SpPH/NGhDog 10 | RAXca9/UCcQXnPqWBKMwNiCDEh0CAwEAAaNhMF8wEwYDVR0lBAwwCgYIKwYBBQUH 11 | AwIwSAYDVR0RBEEwP4cEfwAAAYcQAAAAAAAAAAAAAAAAAAAAAYIJbG9jYWxob3N0 12 | hhpzcGlmZmU6Ly9naG9zdHVubmVsL2NsaWVudDANBgkqhkiG9w0BAQsFAAOCAQEA 13 | uYeK66sCyp4QT7iqr1MSajRzSMD/q7tn4l6zsa3u3Trs1kDURqhhpnARL4D9Vmc5 14 | xNqweReNXWWRJ03U77pvUZs0YLa2GbgDO0Fy+WZ455sRL9E6MWz1WsYkGMLzN/fg 15 | bWgOKHNTYCCOvEjkTcy/uiDQm063/6Hjs/4GqwniM63eBeuLEFHWyj78NDjO5cna 16 | IM6lAa9bsxqAF1Z1cA6UHUgdgt50eaGM5RLAQ4uzHYx7bYCNdKLU+AqndNorq7Sd 17 | tzPduGFSAma7ONjctdlb7UXjruCjgw2TMOzRJ1rq+BZn8N0OXTi7iVj/FoY/Havw 18 | /EmJBSvaBdFuua0TYE8F7A== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/certs/client-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAx45mEFebJM2cOSAi6smmoCMgvODZYSOfJCihFXDdvmdQAyDl 3 | 70BwBQnO3ZHcwT3XKX8dl7vylhfTBzVJXwsIzTvCpHDVoVQ/4VzChONE2IxFVcoA 4 | bHkzQhmEmDN3E+qa1y5bjs5cQoMNMYS/oy31dDOP/otM6E7HdCxNFIwjJXTRA/oA 5 | L71fT5YDUEqgBuQs3zOBrFdmx+LynO2VK5xlYa12fHQ/t+9xeZC7l3IAl1nr/F70 6 | 7YxKFZMHHm0Gbd/VxLfl9d+fUQRYqX3yP1+eZimmVWqOtSdVcnFj5cRde6UuJFLV 7 | Kk8f80aEOiBEBdxr39QJxBec+pYEozA2IIMSHQIDAQABAoIBAHbdhkQDuuDgLEcG 8 | smXB+aN3aR+4myM2cau7G8BGu36X0Vwbs3qgmlkV74ehQ6pDaK9KDVl9VVE8HbI0 9 | dmDLlNGS4CzNHSL8qRRXCXLYYQDQBNjF+xyh0Pt1cbqrJSnS26qC7XyRxPjFUQ2G 10 | 8hOD46n0sLfBR+00R7AWV09+7cx3ymL2qThJh2EIhq09LKLbHG/7pYBtGoopRBJW 11 | luksQlFymuwMO0CD1C2W8LgQhvzMEiRDdZNY29oBX18KKpZ1jqmXu4VC7uj4Z1IN 12 | 44sQNkkAJsndH8MQ7aLE7Dhx8hmCEedodQHPGVSCIChlMxrGqmvF7joEHdjJPZK0 13 | zz4c9MECgYEA/3skBp/k9NMDg7cix/VL+r6FPXNngrekd6eDGy6LNRk1crwD/pas 14 | 9OQp+QlGU2YFdycDE6UyI9gCb1oHUgH1oDRrvyOppTElWM7d6YlguDEv6GNpQfJk 15 | oI9YligiMcO6x+xjGBh81hhaYY6dRh2s+PgN7AmjkbONm6PMCuq1/g0CgYEAx/Ys 16 | 0dWn2blpDEeLNbi8pIDHhIj6hXvkmLHW1KwnpxZraHGV5QoNBAVRQt+VyF974E7w 17 | JfA3ypT10VzCcuJw6fNHj3Vu+8whpt37PLO9zAAQTxxIyIAMnak7qKxW2ZWknTq7 18 | /NLWvzezR+HtODuhG9cOOFPTpv0KKlFXtYrocFECgYBY1nQXjaAq9flh4tvIVmbe 19 | QUPJs4iJ7tvU873mRNAJXcO1KuXksHZiDbj+rRf0RiSeY0Vxnl8KEcH/AHpNLPtB 20 | gxj4dSk3lRhcgkquO6QTSJ9VGsRuNyCAqHfwdvI1Bc+8V1m59kHqnLtI8zODPyx3 21 | wqHsswlaz+ns9g8suKMiPQKBgBELKJLSFTZ3mT6UsobnshyLZXYkfsX142wobFlA 22 | OzkArjL+y3n0O1vGYEDE8e1cRiC+WbXCHd9EhxdLQr+sEVe/hq/xoH4RziR88zcf 23 | UuQadUlo7cM5NtoRXKZp2hU9rgRAx1krV2aBBuTvmtqaKodG801Vx8qJ8t3chQ9S 24 | QbGhAoGBANuSlo2N3lNfW3YN2+5wXdMUiILAAmpgGQ8y1vmrgrDumwVjiC8+2+Gq 25 | WlP0vO66RqpqNHioQAZ6GCOtMjoGBT14k1KxYZ8o9e57DtIYKgzGm35DLaGdvbjp 26 | 85+djY2Rk0vNx+SunpL0kIqaVCpAiXPHo8+Kar4Xiio2ONkLkvXc 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/certs/https/Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | .PHONY: all clean 4 | 5 | default: all 6 | all: clean root proxy admin 7 | root: ca.key ca.crt 8 | proxy: proxy.key proxy.crt 9 | admin: admin.key admin.crt 10 | 11 | clean: 12 | rm -Rf *.crt *.pem *.csr *.key *.old *.attr *.index *.serial 13 | 14 | ca.db.index: 15 | touch $@ 16 | 17 | # ROOT CA 18 | ca.key ca.crt: ca.db.index 19 | openssl rand -hex 16 > ca.serial 20 | openssl genrsa \ 21 | -aes256 \ 22 | -passout pass:some_passphrase \ 23 | -out ca.key 4096 24 | openssl req \ 25 | -key ca.key \ 26 | -new \ 27 | -x509 \ 28 | -days 5000 \ 29 | -sha256 \ 30 | -passin pass:some_passphrase \ 31 | -subj '/C=AR/ST=Buenos Aires/L=Tandil/O=Split/OU=IT/CN=RootCA/emailAddress=martin.redolatti@split.io' \ 32 | -out ca.crt 33 | 34 | # PROXY 35 | proxy.key proxy.crt: 36 | openssl genrsa \ 37 | -out proxy.key 2048 38 | openssl req \ 39 | -key proxy.key \ 40 | -new \ 41 | -sha256 \ 42 | -addext 'subjectAltName=DNS:split-proxy,email:admin@file-server' \ 43 | -subj '/C=AR/ST=Buenos Aires/L=Tandil/O=UNICEN/OU=IT/CN=split-proxy/emailAddress=martin.redolatti@split.io' \ 44 | -out proxy.csr 45 | openssl ca -config openssl.conf \ 46 | -batch \ 47 | -days 365 \ 48 | -notext \ 49 | -passin pass:some_passphrase \ 50 | -in proxy.csr \ 51 | -out proxy.crt 52 | 53 | # ADMIN 54 | admin.key admin.crt: 55 | openssl genrsa \ 56 | -out admin.key 2048 57 | openssl req \ 58 | -key admin.key \ 59 | -new \ 60 | -sha256 \ 61 | -addext 'subjectAltName=DNS:split-proxy-admin,email:admin@file-server' \ 62 | -subj '/C=AR/ST=Buenos Aires/L=Tandil/O=UNICEN/OU=IT/CN=split-proxy-admin/emailAddress=martin.redolatti@split.io' \ 63 | -out admin.csr 64 | openssl ca -config openssl.conf \ 65 | -batch \ 66 | -days 365 \ 67 | -notext \ 68 | -passin pass:some_passphrase \ 69 | -in admin.csr \ 70 | -out admin.crt 71 | -------------------------------------------------------------------------------- /test/certs/https/admin.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEqzCCApMCEBgjn/3sbWejQ3LP4d1e5T4wDQYJKoZIhvcNAQELBQAwgY0xCzAJ 3 | BgNVBAYTAkFSMRUwEwYDVQQIDAxCdWVub3MgQWlyZXMxDzANBgNVBAcMBlRhbmRp 4 | bDEOMAwGA1UECgwFU3BsaXQxCzAJBgNVBAsMAklUMQ8wDQYDVQQDDAZSb290Q0Ex 5 | KDAmBgkqhkiG9w0BCQEWGW1hcnRpbi5yZWRvbGF0dGlAc3BsaXQuaW8wHhcNMjIx 6 | MjIyMTYzNDQ5WhcNMjMxMjIyMTYzNDQ5WjCBmTELMAkGA1UEBhMCQVIxFTATBgNV 7 | BAgMDEJ1ZW5vcyBBaXJlczEPMA0GA1UEBwwGVGFuZGlsMQ8wDQYDVQQKDAZVTklD 8 | RU4xCzAJBgNVBAsMAklUMRowGAYDVQQDDBFzcGxpdC1wcm94eS1hZG1pbjEoMCYG 9 | CSqGSIb3DQEJARYZbWFydGluLnJlZG9sYXR0aUBzcGxpdC5pbzCCASIwDQYJKoZI 10 | hvcNAQEBBQADggEPADCCAQoCggEBAKbv6urgH3pmOfO0YtyCANcUjcmt1fQaCPTF 11 | z0lKvLmOkCnfuedEihbfWGCvKRqimbWp+EXCEW42slvo7rgD1gRMXB55h1nuvikR 12 | CkCATN+BiKFqRxuRvg+r+a05y2qIJRsMUP8tsuQH0ARRp/nxV+skkpt2kGocF3tS 13 | rWIglnYvYwKfyaNJ8U51INNOOOXRZs1GtsEcJZaIQaBgpEwkqBLe7nx69390GUbc 14 | AETCOd8kY3EraW/+FV5Q5snE0RiuEyC1ZUpD6DkI2iZ9ejO7uDnOKSITJnUwGeGt 15 | Qcpxzo42kXNLDYvWL6SI8wsR7jqVmDLS6NV+xihoT1kVeL6PgLsCAwEAATANBgkq 16 | hkiG9w0BAQsFAAOCAgEAmLZpSa9fi1jllbseD91H+XjSYAx6aW/R/HtC3fI8cWFm 17 | oYvRP3bNtCMD2apg0OO6VX449LgoHYSC10ZkoIsaiwwL0soTnI4FEwovOSgXcz5j 18 | O7afaeUVH6geMlvV5YzMNOqu2oMS6Nwr7IPtKCKybWOr39oq97VzxRS0tTpsyRBC 19 | uK/TD/9Mo85icU1zkJBFHWqqMZH40dlJw6ZWjp7El4WeXIbwgmEo9WpgqG9nYIo1 20 | lDWmIyTDLYopeqNMvOhodgXKjF4JTwHUO4HraAQsa8LhYK6LWfBfMYM2BnoAZIMR 21 | tEyc/eqC/VJwIMlT+gA3ARV5Dkrm0ksNfjKgzYZO/0RrQmyM+JkfD1z2tdGefsb2 22 | db/+WhcC0DWoQ6FzISCpkLYgzI35rodsD7u84e2kjqtG7HfWIyqoxeAroQOUkZ87 23 | iUC87WuKOTFhZ+UrjoRKnwfT5lqhJPTv4ngbLifmEV8eXl0CP5ZrI8pPodeiBz+y 24 | 2yVDsV+eeLXjl4bHpUT4MKBxnWL2U1BCg4M3WZx393vh9OZ1Awj8fy+Lg5g3tclV 25 | elYPDqSgBB6Amh6wo5U67KkGqsLWhxxq4U8BcdAiIIXx9au9x01DS5RiReC4lKU9 26 | kfnh8eZf2TiivJb/WXpz33+efGAzO1OueNjMHcgONFAvrKS0/0sP2UhXEQsG3+w= 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /test/certs/https/admin.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCm7+rq4B96Zjnz 3 | tGLcggDXFI3JrdX0Ggj0xc9JSry5jpAp37nnRIoW31hgrykaopm1qfhFwhFuNrJb 4 | 6O64A9YETFweeYdZ7r4pEQpAgEzfgYihakcbkb4Pq/mtOctqiCUbDFD/LbLkB9AE 5 | Uaf58VfrJJKbdpBqHBd7Uq1iIJZ2L2MCn8mjSfFOdSDTTjjl0WbNRrbBHCWWiEGg 6 | YKRMJKgS3u58evd/dBlG3ABEwjnfJGNxK2lv/hVeUObJxNEYrhMgtWVKQ+g5CNom 7 | fXozu7g5zikiEyZ1MBnhrUHKcc6ONpFzSw2L1i+kiPMLEe46lZgy0ujVfsYoaE9Z 8 | FXi+j4C7AgMBAAECggEAUsT15wrE0L1K0oiH0+kpXXq1al+ki2k1M5e4VRCXTjFf 9 | TUO+OuqCxSBsA1QVvz0LlUT28i9s0QaRnHx7kAVm4a6ypfF/qJl084udV6nFc7QX 10 | +GBnbUXvxHlyS+8x6loie6y5pCwWXHV7MAkEjiqZet8hSa+ZnuLayayOhu69a076 11 | On1X018hoZRrWlTBV2lzsqOUbLrxdhRdmZZ8W7TqqD7KtQpJr81Twln24TFmXTHL 12 | 4s5nhaTo5jq1J5pSTYoGKt9HD4Uj88xFkTDI/+dIzck+2s94BkwQaJ/y87zalfBE 13 | wUFwc/8O/bdm/ttUTLpxZTSCVC8bLKo71LGkaVGCwQKBgQDEvPg82CTRc6YdBWOg 14 | PhKAsQq5N7N/58/oI2aHovpX2pFs1XFzAERa4McXQ9rFiHRCYQ2BTjDCM0M6KNDF 15 | QPmHZzHyVvRvDBzhK9UU2h26NfghH4FqcpY0LcFiPEm1mMXm2Ufii3VWLywYPz8Y 16 | b5xJ00Q8U5CEjSSaPoPfs0RikQKBgQDZOOkgifu5ObIH5SwpqdHAPv/ariycyaJO 17 | 8oGGo6Pv16vBgAwQfrCnOR00S8MjfGyWUeosvI++QgOcUWrWUWhpVhvFJard3pfa 18 | Lc0rsNAT0Fu88cUgWdyC4wP3+xGgNV/RJXlQfcI36gHCXwVHdi5Plt1Bp9B7JufP 19 | ZP32zhQ8iwKBgFrzJSsznOm7Ngrqh+D3cSRPNC7l4jR6HPIrE2YW4PamU15l2hmZ 20 | AQCmM0O9GbEB4QUiytSBKidM/YIwhjr6S6DeAwgOTNfdWKh70/jc0KtZ8ciWQQTN 21 | zkR29pSMXGL4Kl0LC6FeaTMbgZ3/9xI73pt+cGgXFZNBkK9BwUM0I6QRAoGBANSm 22 | 0CLYsiOMhesQwYEwDHUlt5e/d1EuW8Tpxz+lp3G/MxfFYQos3Id4dEyj9q8gubUX 23 | ECcnmZjqS1qWof6Zx5uHfrwrufBmX0ZqHDcvayRaj9SS4yZekm9YCqSTl5e9aMX4 24 | 56CS3LWcUoiUOTjSS1gDGyuRO5m0Zq8z8SPSbyEtAoGBAJ4G3vXeKKvBE8wj1jBd 25 | ggttqzC9PLL/FvDF62b1TFeXuyqG206U07G7LUkCvkcRmxzrMNLHdNslxVOeLjzd 26 | ETAwXjXzBPkJ74WqGi6CXei3E7bgUPC9nR9TyV8eLa1xaz7J7ryWVSSAjxQoouET 27 | 4mIehHofqUiKJAqiF616b29v 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/certs/https/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF/TCCA+WgAwIBAgIUeK3wnW5wtYBEKpwkXgrSC8719G8wDQYJKoZIhvcNAQEL 3 | BQAwgY0xCzAJBgNVBAYTAkFSMRUwEwYDVQQIDAxCdWVub3MgQWlyZXMxDzANBgNV 4 | BAcMBlRhbmRpbDEOMAwGA1UECgwFU3BsaXQxCzAJBgNVBAsMAklUMQ8wDQYDVQQD 5 | DAZSb290Q0ExKDAmBgkqhkiG9w0BCQEWGW1hcnRpbi5yZWRvbGF0dGlAc3BsaXQu 6 | aW8wHhcNMjIxMjIyMTYzNDQ5WhcNMzYwODMwMTYzNDQ5WjCBjTELMAkGA1UEBhMC 7 | QVIxFTATBgNVBAgMDEJ1ZW5vcyBBaXJlczEPMA0GA1UEBwwGVGFuZGlsMQ4wDAYD 8 | VQQKDAVTcGxpdDELMAkGA1UECwwCSVQxDzANBgNVBAMMBlJvb3RDQTEoMCYGCSqG 9 | SIb3DQEJARYZbWFydGluLnJlZG9sYXR0aUBzcGxpdC5pbzCCAiIwDQYJKoZIhvcN 10 | AQEBBQADggIPADCCAgoCggIBALf4EuB+gpO79PsiK5IFChO84Hau/BJMoEHkCZb3 11 | VXvRZBu71LTVlC2WociE6fCVzPGnclvIedLBxXb6kaymyJmMe19TCtGRh1UBpeUU 12 | JKNExNT12xZnhRxKRNsG8PNIwafpbdDeaKDfAMnT8XCEbLPCRY0J3zWpMQUcr2bb 13 | s5OI2nZgi4C/Z+PeIUxmNa3CtVE6QO2O5RSOtMHQZOVpgQaIR2zMRF4ZUr1SfPQF 14 | LwX2Yhjp0/eXaqsn3fwUJG5g16skuN8WdXd/+uyoCd1v7zS/bdLNPflxx1mc/hAk 15 | ME+d3DGf+E+C9pxaYj1G1xPDZfV3Jc8vnZxZ7F9sS4lgCYPyvgLUIf62o06XtHzT 16 | 92sHeDLS7s4fNV6ZXfuV5FKI+BXcCDGEMnDu/66/0A9T5A8edw6ShmCTH9cvwaDE 17 | nL7633VxbarJh4kRvMIwjEEdug0TjnxEHf4JgUaFf/cosFjzeltZ1U6qwY4GMwz4 18 | xmbLij0gJRTr5iZ9QPeKy5crADS9Yr601DLeRD3HI6RqGtIGFxk9ulLYMV1EZ7sB 19 | wq57HmOlIc8pt37P12WpZjvKyKu3jWiGzCHHbO4PIUuHQ1nB8JHt0HmCbVsrL8Ic 20 | cSUdQaL19uhS8zGFhUebl4WfPytq5W/ANpRmWij/DpnnzqBHXllRUtmhKMaQttgT 21 | M2nTAgMBAAGjUzBRMB0GA1UdDgQWBBRMeP7AADeKY4M4mp8oZK9YbEWTOjAfBgNV 22 | HSMEGDAWgBRMeP7AADeKY4M4mp8oZK9YbEWTOjAPBgNVHRMBAf8EBTADAQH/MA0G 23 | CSqGSIb3DQEBCwUAA4ICAQBGhTqMpnTI3q4R/zAmnSJg1U8kk37PMMb+2fYYRFxl 24 | 8CDoiETg2QYDSgOoa0hayWY2ARpci9xYCx4+eTKEYostv+ME/2WZJZUFH8IA4y7A 25 | fbv3ZtBOEN8462AmaoBp+AWPuVeQHh1ejRJ7R1dMUVF/uHhaghDtzmpH3h7wjnVS 26 | YLfuV8ZwFWOom9gADXuVY1EZoMmWhzeCneNCqzQeYVkVltcajn+KB+Mvph1m7ndV 27 | uiMBDE3+y/+eDBZD3sgbUc2fcvNaDMRS0WB1++TZwhUHhfIvd3FROnXL6rIqlWV1 28 | rs4iHTsRcAgcwHx3F0l8qAkWG97CmaIt2+hAOQVIU3edoHKnlVAlRSLF2u4cdKmc 29 | c9GYthwZviJO67NjxrSgUU3OL8Nyc1Rb4YQevggeyFDu9vwokcfJ0a83rczcomu6 30 | VPcBpi9fCs+VvmpbfZa8DQBFDRqij6sj8ZOdCDuaLN/xenLPGL9bRKIBcGOyNZa8 31 | Me+s/uhGdsyooHB2lCxI6eS4ukRKuX59l+LuyMWLU6DATN7ExNjnMeZ2bhkLliNE 32 | qUbQqs44tbv3xt0zxWZXNKQQRoWq+cxOEo50RXy0UUDnyKSLqjzz3I3DcJdRRLUq 33 | TYNMLFaBQ4QXr9XY0z3MEH3uDWDT/9A3riCW1PN1nxM4go42DXtHaRPXojRrVao8 34 | hA== 35 | -----END CERTIFICATE----- 36 | -------------------------------------------------------------------------------- /test/certs/https/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQI1bUuCwK6kDQCAggA 3 | MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBD336z1uf+tq+JkvvMVpeWLBIIJ 4 | UMyEdIJWpsmXvKn9V6rmnObGB2gD1vEymDk1lIEmF8ys1HLT8CnvUyoHuGUxANWC 5 | fhtJv2cnlnI7R7DDV6vCb76KX1b4lcS3P5eMAQvfB2kyy8kcDsA9HCNCdMicbuPk 6 | 16V9Nb6W83c3NV39u1pC7pErpbP110gS485pZh42Y3Dpzk08QEsmqtzChO0N5ke9 7 | GCI8LCBUOeVWW25v8sx3t93suH6AzxeaNUu3jGDV25QcCA89aSeER9sGxIDnn6+X 8 | HsVfwmJeEzaDkHgUMjrMKEyOy24oOK9YvOn/W1aAw9EsEz1DOYmIEYHWjLraXWBn 9 | w/aPoCuxYgTNKVbmI3ujy3hkiiaLp7ixXMn7yNNeyJxQePJGb63m7O3yP0SJO8kP 10 | Gr+c5S9ET3nyGGmM+vxNlcLR7nlweBoXFV2JAY0Tp82n5ktAfOYzUCPlerT4nR+q 11 | FIyUlxRhjTeyNvh6nUzikFMWkmuj7o2AIcI5VSbRo1LY396vWQB0yhoBFfmapipY 12 | bpb3urSe0z3tvTLuWFsBPC4LQIUxPSAYNkaLgiJoH8KTXg6GOwK0309dc0VyPiF5 13 | bEmcFdl6PrbErXU3Nlib3kIoPKTWsPFUnRakMOnpLAeZGckHoC2Eje9FQE9I4piD 14 | Q3EG36ejwZsPiq3t5dhccyiL6pwiCfDQYzIveaRCDmlKpTXpLdbhPwYNfE/Kyq4h 15 | lt3TQLVAUnIfYbtrz0cWqQZJCBlfvVYYhddtohjtqccsLEKkNejAjvUzb823424i 16 | Z8/g5Dh9HuWifc7WViFmYKP52pmofJbyNcwl6s3A1LPz/k+Tr9MOWaCEOcCmD6sI 17 | X0XtO4wZtWSAzo/xOr1ssE9KQetq6cJFc/0K91mCUlSs1DCKyG/gnDjVYq/p3oR1 18 | 2MMXqLeXdea9rCpPUMYP+PeZXka8UV2hXPUxVO6+g4chppfXrj7hMqlqHzIoNmkK 19 | cwP48kbZl5FFxA1IBpv4VHVrZX7f8IiWoyZbFKrxfFu8+3qJgDrUjw4VyCzMcUYY 20 | 2qX8jHoWgzvLS2azLIuHjlKT9eguFrYbPVsZL5dNLpZNdCGf0Sbtf8JDl7TBOljd 21 | P0CbgKiphKeCdtnS8JPMrLpjJ/Me+kQPy/rHU+qamEFNk1JompBI7xniTyCuU4Yr 22 | awziOtlSL/k8xw56sSlSkPY5X2KDdki1Ty7g2oB7EwcW297yuGqRp7ikQhuswCsf 23 | zC9fHYapnBElujVc0J0oMhG8TaXC8k2ziqVqr7sDfElcwhIlGt/kRGe62twqQb+R 24 | NmdRc/aowW6uzjGLkVFMZv/3FDnDcfzo09m2QyNGhTefskdyYXI2Uz/Ot9pLmkqn 25 | n9TkIxN8rDovNUzsQUKLFYxfJISe0UnZt9t+WTLp/byXheCqng+Nt8gDjqkQW8YM 26 | x4ohiLfOdM2B/BOk8J/J0z6D1gICWYts1cIVx6dyY7R9gS6ew4pNfvznRlIi2Z/P 27 | trQtgCYiH6/tlSMOtWc+uTDkQ4f7oNZe/FEyaUoPCkGFYOxQGRtNoQc30oVEOMVr 28 | U89JW1vgQQJvTU1BWbzf6Y0i/yhvE75qND8AAhpnkHnVQxb2EzB8SodORXstAhzR 29 | H+qjzrFSU7IWzAFai+BnYzOeTNHDjnRWlIaKOTr7ImKAVUw/XBvfQ40jqzjOKgAa 30 | LZyzkX6eUyt6SwqS9S6vvBMOIJd7MiVV/jRHwSTDhyUagoNdqCN88giv45+/vjSO 31 | Pfk1e63S3EPPmJaxBBX5wlQ3aDXavH/9NYDYvKShj3Vnv7PzYSf1LqwkuMcRFsbE 32 | QhmY8lVdkQerqvgpCgTfiiuiOx0zFmrCOfzY/ACcM3xrhGuXaBNsY9I7bbinTM7H 33 | 0OSoOMl0W+My8A4q9W5lD/bDpwbBHDmQobY7GGbdgSGix0EuPvr/iY4KnUCbBnVd 34 | 3zhPpZ6Nm/z0vl74y+MugCAplccQx1zMM4vzeXTjpsvSo3k9sl7bLUZsMxdH0dsD 35 | 6rM1o8NsFsoW0HgaZ2+Y8fFA7KQuZ1z+NlwkJzH/hXSMNkNzQb+9pcJAgF7RUUWe 36 | C9nhAiVJ90TuhZuv9rmBXLnGsbDfIqiD+cF0pafb8TLLY9E+4ssAVj/tbOqjY8MK 37 | ZX3sa9EqZbz8MWgusI6FQOOBRmjtsucjT88rjFzL51VjrlvKDPiOjqjCxCPu4hUM 38 | UJM8XaIcWHqTzFiOTmb9hZmiAtXygW0lcCbgd7oGWHnzS0LMyw1Q3KdlYFE+3NKx 39 | 61DIxAU3kmtiJo5LsnyUyDpXnXnDICbVNi9sIoKcHaLwwCti1BkG/f2q5R96wGJT 40 | YK98ADy2FDNbB1vv1DdiNuwDRXEpdQ1RnjsIrSBYE1awjLBI/xXkwD8oQeoZ6Cmv 41 | TziEM5iFRYoTQBmEvSRJa2hHhFXEcRHaF1VuRyHGXz1OdKP0powUtaKQznEjEvNW 42 | E/1PkZ/HErtqkklhamAXA+rAPehejTYJERfPgIG1OLeXGSphsay/QqCwvsOGabeC 43 | v7ieYId9rRTGPEF1UA1OneGyQaG8LpD7Met2u0rjFoPjwb9EOAARXRgK8JJOsDBt 44 | 8uFqUt3Ba40pnQOeyT2JpPFztAVvwZSAh/C7bsm+DepdlalW3XTFdx/tGqQ+jW2d 45 | adTw1h9GGP7AwRDgphn10rawzDF3vxe0lFhITInpIk1hAeux8OPNtz6xjM2i6Ge8 46 | 7SeShYWD4Ej6xirGKMU3hNERUIjGaq61Pq62ldx2MyGgc3Dw9a3Ye8YtzA7KgpzN 47 | xOFbULr5uqhVQXUeUaO7hQpxZ994RRNNhvOS3WJSXP8zJorEKfDqKqft2/Mw8cCM 48 | Z7KBYIy9TJ117huReMLZ+753q38wrvXB97MdAuSoBfXTmfhZ71IgUBqwfsnO/JAK 49 | 2J7yPmykvFq4axXX8rYT4De1GfNLJ4KXMCijMIDRMm4Xl2AfbSH1q26kiwHEWnIK 50 | zlqBGjYZFqIO/FV/ZrtBk94YaXxCDIQ/ihSontbm24fsDZnnAJqv33vdvobMSoe6 51 | gRaPFYvLbFCjRj6AG5BMX3nJzFlqNVGJyTZi3s8tud3jIlb6PAtUmGO0ovwohEw5 52 | PPBb9rOcQLvVxBGMXhUkA9yRRmtHg5yhqm6UwNAa6bCJbnXtz/n2OGR9GpVXHVUO 53 | J4VLBML39YV8i7H1S3HqXExSPaMenIfWn24t3QeZWgEe 54 | -----END ENCRYPTED PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /test/certs/https/openssl.conf: -------------------------------------------------------------------------------- 1 | [ ca ] 2 | default_ca = ca_default 3 | 4 | [ ca_default ] 5 | dir = . 6 | certs = $dir 7 | new_certs_dir = $dir 8 | database = $dir/ca.db.index 9 | serial = $dir/ca.serial 10 | # RANDFILE = $dir/ca.db.rand 11 | certificate = $dir/ca.crt 12 | private_key = $dir/ca.key 13 | default_days = 365 14 | default_crl_days = 30 15 | default_md = sha256 16 | preserve = no 17 | policy = generic_policy 18 | 19 | [ req ] 20 | default_bits = 2048 21 | distinguished_name = req_distinguished_name 22 | string_mask = utf8only 23 | default_md = sha256 24 | 25 | [ generic_policy ] 26 | countryName = optional 27 | stateOrProvinceName = optional 28 | localityName = optional 29 | organizationName = optional 30 | organizationalUnitName = optional 31 | commonName = optional 32 | emailAddress = optional 33 | 34 | -------------------------------------------------------------------------------- /test/certs/https/proxy.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEpTCCAo0CEBgjn/3sbWejQ3LP4d1e5T0wDQYJKoZIhvcNAQELBQAwgY0xCzAJ 3 | BgNVBAYTAkFSMRUwEwYDVQQIDAxCdWVub3MgQWlyZXMxDzANBgNVBAcMBlRhbmRp 4 | bDEOMAwGA1UECgwFU3BsaXQxCzAJBgNVBAsMAklUMQ8wDQYDVQQDDAZSb290Q0Ex 5 | KDAmBgkqhkiG9w0BCQEWGW1hcnRpbi5yZWRvbGF0dGlAc3BsaXQuaW8wHhcNMjIx 6 | MjIyMTYzNDQ5WhcNMjMxMjIyMTYzNDQ5WjCBkzELMAkGA1UEBhMCQVIxFTATBgNV 7 | BAgMDEJ1ZW5vcyBBaXJlczEPMA0GA1UEBwwGVGFuZGlsMQ8wDQYDVQQKDAZVTklD 8 | RU4xCzAJBgNVBAsMAklUMRQwEgYDVQQDDAtzcGxpdC1wcm94eTEoMCYGCSqGSIb3 9 | DQEJARYZbWFydGluLnJlZG9sYXR0aUBzcGxpdC5pbzCCASIwDQYJKoZIhvcNAQEB 10 | BQADggEPADCCAQoCggEBAMF6GxyDtoapI/XXr/Ty7WMCj2jq8EdWVXJiRqLfJzUQ 11 | Kz6CyBrXhN7a3T+AAksImtOAifNYViC2t6KxtVy/PZLhKy44+Qbsg1JciGPSmx1x 12 | LtNegkRzbjKL5jx5x+IIjl/MeyJ2ogK/Sm+GI+GHKXRGqESiymdAWO1IP1bI3749 13 | beFJlSHrpFXekTCLx/gMg4biyhhUClyxCfyCT1s+FdHn08zKpMgnQre4RzYNLv2q 14 | bzBo7146yNQdlzlAF5rm1IrDFNMcGkrJQn0Wkyi2XLlBOaWILOQmliWNRefdyhfh 15 | I41msuafL9i8H024yRAUE/0HUH81v8+KzAbYbUHodsUCAwEAATANBgkqhkiG9w0B 16 | AQsFAAOCAgEALbp8ywp3/CPUsXXUMrFCrkWJNCC2+JNbss9zzmOoHLtOynIFO6hC 17 | rR6S6J11tGQBTd7aWbSoQVI+9YRCyDjI9DhDsebCttbZeqF16ynHQQnAX6CotW+3 18 | nseP0czhUpGBgtINNYKJYrhMNe9UlSfuTcE7bvSxdcF0HiOh0QXwy2yMrJaXY2WZ 19 | ogKGI/dpHPb08NqvS0Yg0ZNme7K3kfo+Jga4RjrcVRCVPLyB+xaiuEKnQGFqCtsN 20 | Bcv1UP1hAU4p4eeWSre7gQLZ5qvNpQiXswtlqQ77/iw8izIv5rEPOBCSPlQkeNP2 21 | cC702G0qmEyHJlEWN3qdqNKMsMONWhnzmswpzcXypTzmRw6EaatNAC3mkNfHnoLV 22 | DPuAEoc2NXGPqQrYUsvF8rngZ64kmLV7uFAMCAS/lYhTkATKe7zU8Bset08b5Iv2 23 | CunVeSfFEvLm9IMKFyi87Fll18UnSQB+3hD6txbaRgqa464AeqdK1n/IcLIZQ966 24 | Ldeg7M5JieK1oCpxXDYiRzsUyAhgLE4SiQueguxoyS+Z6gMUS6BYdAp8S/omB22L 25 | 6/KSzTz63keRYnxq3+nlnadOJ3RqVqIadhtkQx5MH0Bk1FvjVcR5YQfVdylhoWQ8 26 | 5Fo4ZSUhGks4rtp/kdFRHC0iwK6or+B8ieuZ8xrBlwvnZf/tRgkXqqY= 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /test/certs/https/proxy.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDBehscg7aGqSP1 3 | 16/08u1jAo9o6vBHVlVyYkai3yc1ECs+gsga14Te2t0/gAJLCJrTgInzWFYgtrei 4 | sbVcvz2S4SsuOPkG7INSXIhj0psdcS7TXoJEc24yi+Y8ecfiCI5fzHsidqICv0pv 5 | hiPhhyl0RqhEospnQFjtSD9WyN++PW3hSZUh66RV3pEwi8f4DIOG4soYVApcsQn8 6 | gk9bPhXR59PMyqTIJ0K3uEc2DS79qm8waO9eOsjUHZc5QBea5tSKwxTTHBpKyUJ9 7 | FpMotly5QTmliCzkJpYljUXn3coX4SONZrLmny/YvB9NuMkQFBP9B1B/Nb/PiswG 8 | 2G1B6HbFAgMBAAECggEAS2izu3PsxuSS0wWrm625b482ZR644x0XtbrvLBkM94Yj 9 | TLx9kNSygYfNlyvl+OfULJ5bZkDmZN7CiUN5XDpfnelsQppLGXNCpe3R7RJPifSs 10 | 2w5peJOC/ml/pc+TZBqQn28cCS0y7R4wvXILdyIOura1a/cFK6QtZOJ1aXZmmoc5 11 | LgQ1+6AVXqdgrgWHuaEujRyMmC3YxGJQlyYHiqxfZNS5+qvZ/ve1mQVs1fWO85Vc 12 | 5fTYhBl42JRTDAzVjvkJAm08pGqn4d0nnRCK+lpb537uO0FEnXkXZiVKHjfI6YHb 13 | OwMLeCzUlx88W1i93Ofaz+zsGyA3sAu+d4Rv+I8WywKBgQD58gkSVbgExgfRs3oO 14 | lUqPQgFbFrGQwvVkAotEWg8iA/reGJMg5Yka3casCOruP1b764FTtodG5Q01s1cU 15 | NH9OSXMkMveZ1AALfydCHdoVLy4URCscP8ynBdPVBoP96o1u/n6oOODNKc1BVetY 16 | +gLUdb22q1sTt+X8lDo1SfA3lwKBgQDGKeXEAhreY15EzcUuyc0NRr0bO/V/r8fl 17 | SBDxeKNNsNSIY4Pzw5J9QKUshKsFSStU+BZ2Mq2Ue/kojwGoFd2eyN51c8GzZW6s 18 | 2tiH5kgHmYVBbi1EDEx0ot83d+8gNvDFAseGb0NGr+B8S+1bLwAO27Yj3yp5qI3m 19 | T4GqbSCwAwKBgQDVAzZB/vXGc7MEP649MXSKpNkc9Tk9Qzn5EsX36bzN45BwqYby 20 | WUzArdN8mFkH1MlgB3R/kKa3f8wDQSVsXdVFNgnABwPHgMrNAX/GtERBG6Vsti/7 21 | clAK5EeFXHku9C+3MYNmAJttnjuEfCIIAYJZ6UJWpLEJHAgQe48kDTCBXwKBgBmj 22 | TA3K8+z30Dd4o91E0Jm6IDdIz59gf61DYKXNJNulWCn5LhY4pFg+J+CVnYbGi6un 23 | mUhbkCeYzoiXz/AOPCkR9e4eNt5d7i7A9ajHe2Q4UYxAk+ys5qtkcxq7Ep4JXacv 24 | j97twDeCA7oxHJligFBrzqnfcqBg1VMJ0E3bZpI/An8B2MvyhcOqABsUWP762wFd 25 | mQMdk/YY1DBRTbYjYgQ08yI4qcbNuAZcEhDgxOniFe2xsjyg/I3ZEWMTasFgxfll 26 | B6Os9sEGGVzHLQvKQ6Y6UQYIxmE+7Wp6Q1gjfft+uqdTBdWEIveaWXqR+UtJrbO3 27 | bDw+5CBIGNqP2bAiV4PA 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/certs/root-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC0TCCAbmgAwIBAgIJAORGV6R7xTnaMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0xOTA3MDEyMDM0NDdaFw0zMzAzMDkyMDM0NDdaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBAL0VKOObUFUtHnY1QQ/UHD+4CNvNxMV96m1yf23rJLz2lhH6u1JuUQTOk1Xs 6 | Ff+peeWgvJ4TBQbIafCsZ4KuRKtl/rTcWdLC2QOy9BxUmCboHlflcHI3wKMTASUf 7 | 36qlOMgzHNnkQdadUVynYWpwcTnzuxx2EsBTHJO8ExwYLFgQY5wQT8etpu8H5QKi 8 | jkQcG5EafbTti5iE7k84kLIGMxH23JhrhzjPjAQiwwe6E2LD0ZZjEFiLl67gCnvu 9 | MhDklX0TYzch7Jbru8pxuUOnzcQ7mq+TJ/tyCqd902Q4qP1ShW6wIuH4eS43jxqT 10 | B5ZEF9YgZ6xV2R6vPSrbx/MRSr8CAwEAAaMmMCQwDgYDVR0PAQH/BAQDAgIEMBIG 11 | A1UdEwEB/wQIMAYBAf8CAQAwDQYJKoZIhvcNAQELBQADggEBAHe1KiiClo7hY9PB 12 | G+4ioD2h9X8eYz7c2yARqh5jKnd2VA7a5/mz1ta/7eXuXr9dubmCgmkzHnlYmIeK 13 | zBhix8/aC+bU5D8Cc2YM9K5UsyzPWQ3D+Rj1hPI0izIj1wgOeRGexPuGPS0l1tUV 14 | tjBJhjnfYP7w42bwZCINb5Lk+IDGUHjRJaybbW8RSgMTwOf+Ao9yC2jQ1U5f3PvU 15 | 16bQASPJ5xGutwBYnyWylDYuIkAiRFjILs7tdjdI4FZ9jSfqO35a9OfLuCAKAFaq 16 | PxWiQr373MP4RrglRjt0zO9u9iWyi/q0Lgo0oYtnc9HI0LIQszkluqieKDD2uxT4 17 | /sqeA1A= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /test/dataset/test.conf.error1.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": "abcd1234567890", 3 | "splitsRefreshRate": 5, 4 | "segmentsRefreshRate": 10, 5 | "impressionsPostRate": 80, 6 | "impressionsPerPost": 56000, 7 | "impressionsThreads": 1, 8 | "metricsPostRate": 60, 9 | "redisError": { 10 | "host": "localhost", 11 | "port": 6379, 12 | "db": 0, 13 | "password": "", 14 | "prefix": "" 15 | }, 16 | "log": { 17 | "verbose": false, 18 | "debug": false, 19 | "stdout": true, 20 | "file": "/tmp/splitio.agent.log", 21 | "slackChannel": "", 22 | "slackWebhookURL": "" 23 | } 24 | } -------------------------------------------------------------------------------- /test/dataset/test.conf.error2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": "abcd1234567890", 3 | "splitsRefreshRate": 5, 4 | "segmentsRefreshRate": 10, 5 | "impressionsPostRate": 80, 6 | "impressionsPerPost": 56000, 7 | "impressionsThreads": 1, 8 | "metricsPostRate": 60, 9 | "redis": { 10 | "hostError": "localhost", 11 | "port": 6379, 12 | "db": 0, 13 | "password": "", 14 | "prefix": "" 15 | }, 16 | "log": { 17 | "verbose": false, 18 | "debug": false, 19 | "stdout": true, 20 | "file": "/tmp/splitio.agent.log", 21 | "slackChannel": "", 22 | "slackWebhookURL": "" 23 | } 24 | } -------------------------------------------------------------------------------- /test/dataset/test.conf.error3.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": "abcd1234567890", 3 | "splitsRefreshRate": 5, 4 | "segmentsRefreshRate": 10, 5 | "impressionsPostRate": 80, 6 | "impressionsPerPost": 56000, 7 | "impressionsThreads": 1, 8 | "metricsError": 60, 9 | "redis": { 10 | "host": "localhost", 11 | "port": 6379, 12 | "db": 0, 13 | "password": "", 14 | "prefix": "" 15 | }, 16 | "log": { 17 | "verbose": false, 18 | "debug": false, 19 | "stdout": true, 20 | "file": "/tmp/splitio.agent.log", 21 | "slackChannel": "", 22 | "slackWebhookURL": "" 23 | } 24 | } -------------------------------------------------------------------------------- /test/dataset/test.conf.error4.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": "abcd1234567890", 3 | "splitsRefreshRate": 5, 4 | "segmentsRefreshRate": 10, 5 | "impressionsPostRate": 80, 6 | "impressionsPerPost": 56000, 7 | "impressionsThreads": 1, 8 | "metricsError": 60, 9 | "redis": { 10 | "host": "localhost", 11 | "port": 6379, 12 | "db": 0, 13 | "password": "", 14 | "prefix": "" 15 | }, 16 | "log": { 17 | "verbose": false, 18 | "debug": false, 19 | "stdout": true, 20 | "file": "/tmp/splitio.agent.log", 21 | "slackChannel": "", 22 | "slackWebhookURL": "" 23 | } 24 | } -------------------------------------------------------------------------------- /test/dataset/test.conf.error5.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKeyError": "abcd1234567890", 3 | "splitsRefreshRate": 5, 4 | "segmentsRefreshRate": 10, 5 | "impressionsPostRate": 80, 6 | "impressionsPerPost": 56000, 7 | "impressionsThreads": 1, 8 | "metricsPostRate": 60, 9 | "redis": { 10 | "host": "localhost", 11 | "port": 6379, 12 | "db": 0, 13 | "password": "", 14 | "prefix": "" 15 | }, 16 | "log": { 17 | "verbose": false, 18 | "debug": false, 19 | "stdout": true, 20 | "file": "/tmp/splitio.agent.log", 21 | "slackChannel": "", 22 | "slackWebhookURL": "" 23 | } 24 | } -------------------------------------------------------------------------------- /test/dataset/test.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": "abcd1234567890", 3 | "splitsRefreshRate": 5, 4 | "segmentsRefreshRate": 10, 5 | "impressionsPostRate": 80, 6 | "impressionsPerPost": 56000, 7 | "impressionsThreads": 1, 8 | "metricsPostRate": 60, 9 | "redis": { 10 | "host": "localhost", 11 | "port": 6379, 12 | "db": 0, 13 | "password": "", 14 | "prefix": "" 15 | }, 16 | "log": { 17 | "verbose": false, 18 | "debug": false, 19 | "stdout": true, 20 | "file": "/tmp/splitio.agent.log", 21 | "slackChannel": "", 22 | "slackWebhookURL": "" 23 | } 24 | } -------------------------------------------------------------------------------- /test/snapshot/proxy.snapshot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splitio/split-synchronizer/8ab3ab9d6925358494c779f6145360960707bbd0/test/snapshot/proxy.snapshot -------------------------------------------------------------------------------- /windows/Makefile: -------------------------------------------------------------------------------- 1 | CURRENT_PATH := $(shell pwd) 2 | PARENT_PATH := $(shell dirname $(CURRENT_PATH)) 3 | DOWNLOAD_FOLDER := $(CURRENT_PATH)/downloads 4 | UNPACK_FOLDER := $(CURRENT_PATH)/unpacked 5 | BIN_FOLDER := $(CURRENT_PATH)/unpacked/go/bin 6 | BUILD_FOLDER := $(CURRENT_PATH)/build 7 | 8 | 9 | GO := $(BIN_FOLDER)/go 10 | ASSET ?= go1.23.linux-amd64.tar.gz 11 | SOURCES := $(shell find $(PARENT_PATH) -path $(dirname $(pwd))/windows -prune -o -name "*.go" -print) \ 12 | $(PARENT_PATH)/go.mod \ 13 | $(PARENT_PATH)/go.sum 14 | 15 | SYNC_BIN := split_sync_windows.exe 16 | PROXY_BIN := split_proxy_windows.exe 17 | 18 | .PHONY: clean setup_ms_go 19 | 20 | default: help 21 | 22 | ## remove all downloaded/unpacked/generated files 23 | clean: 24 | rm -Rf downloads unpacked build 25 | 26 | ## download and setup a ms-patched version of go which is fips-compliant for windows 27 | setup_ms_go: $(UNPACK_FOLDER)/go 28 | 29 | ## build fips-compliant split-proxy && split-sync 30 | binaries: $(BUILD_FOLDER)/$(SYNC_BIN) $(BUILD_FOLDER)/$(PROXY_BIN) 31 | 32 | 33 | # -------- 34 | 35 | 36 | $(DOWNLOAD_FOLDER)/$(ASSET): 37 | mkdir -p $(DOWNLOAD_FOLDER) 38 | wget https://aka.ms/golang/release/latest/$(ASSET) --directory-prefix $(DOWNLOAD_FOLDER) 39 | # wget https://aka.ms/golang/release/latest/$(ASSET).sha256 --directory-prefix $(DOWNLOAD_FOLDER) 40 | # TODO(mredolatti): validate sha256 41 | 42 | $(UNPACK_FOLDER)/go: $(DOWNLOAD_FOLDER)/$(ASSET) 43 | mkdir -p $(UNPACK_FOLDER) 44 | tar xvzf $(DOWNLOAD_FOLDER)/$(ASSET) --directory $(UNPACK_FOLDER) 45 | 46 | $(BUILD_FOLDER)/$(PROXY_BIN): $(GO) $(SOURCES) 47 | mkdir -p $(BUILD_FOLDER) 48 | GOOS=windows GOEXPERIMENT=cngcrypto $(GO) build -tags=enforce_fips -o $@ $(PARENT_PATH)/cmd/proxy/main.go 49 | 50 | $(BUILD_FOLDER)/$(SYNC_BIN): $(GO) $(SOURCES) 51 | mkdir -p $(BUILD_FOLDER) 52 | GOOS=windows GOEXPERIMENT=cngcrypto $(GO) build -tags=enforce_fips -o $@ $(PARENT_PATH)/cmd/synchronizer/main.go 53 | 54 | # Help target borrowed from: https://docs.cloudposse.com/reference/best-practices/make-best-practices/ 55 | ## This help screen 56 | help: 57 | @printf "Available targets:\n\n" 58 | @awk '/^[a-zA-Z\-\_0-9%:\\]+/ { \ 59 | helpMessage = match(lastLine, /^## (.*)/); \ 60 | if (helpMessage) { \ 61 | helpCommand = $$1; \ 62 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ 63 | gsub("\\\\", "", helpCommand); \ 64 | gsub(":+$$", "", helpCommand); \ 65 | printf " \x1b[32;01m%-35s\x1b[0m %s\n", helpCommand, helpMessage; \ 66 | } \ 67 | } \ 68 | { lastLine = $$0 }' $(MAKEFILE_LIST) | sort -u 69 | @printf "\n" 70 | -------------------------------------------------------------------------------- /windows/build_from_mac.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | docker build -t sync_fips_win_builder -f ./macos_builder.Dockerfile . 6 | docker run --rm -v $(dirname $(pwd)):/buildenv sync_fips_win_builder 7 | -------------------------------------------------------------------------------- /windows/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd buildenv/windows 6 | make setup_ms_go binaries 7 | -------------------------------------------------------------------------------- /windows/macos_builder.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm 2 | 3 | RUN apt update -y 4 | RUN apt install -y build-essential ca-certificates 5 | RUN update-ca-certificates 6 | 7 | COPY ./entrypoint.sh /entrypoint.sh 8 | RUN chmod +x /entrypoint.sh 9 | 10 | ENTRYPOINT ["/entrypoint.sh"] 11 | --------------------------------------------------------------------------------