FB-1597: unifying idk and featurebase (#2160)

* unifying idk and featurebase: first pass

* resolved conflict with master for gitignore & dockerignore

* deleted binaries that were accidentally pushed to git

* combined gitlab jobs for idk & featurebase

* run go fmt for idk

* updated ssh env variable, and made docker password variable in gitlab env variables

* fixed typo assigning variable name

* trying to fix docker login error

* trying a different solution for docker password

* pass registry

* fixed docker login

* updated paths for idk

* exclude idk tests from featurebase test run

* fix vendor error

* update certificates

* grpc needs to be in version 1.38
genproto, which is imported by big query updates the grpc version to 1.47.0
grpc 1.47.0 causes etcd to deadlock when calling etcd.Close()
the fix is to have a replace in go.mod to specify a specific grpc version

* run go mod tidy

* go mod

* run go mod tidy

* exclude bigquery since it is causing issues and undo grpc replace in go.mod

* fix grpc version

* fix formatting error

* update formatting

* attempt to fix formatting

* update path for code coverage

* update to use current branch binaries, not master

* fix for building idk - path updates

* udpate path for binaries

* update job dependecies

* update docker idk tests to use the current branch registry

* update stages for jobs

* updated job dependencies

* not allow idk s3 dump to fail since it is a dependency for integration tests

* update dependecy for idk tests

* update paths for idk build and code coverage

* download featurebase binary from s3

* pass branch name to all setup scripts

* change to current branch instead of master

* updated sonarcloud

* sonarcloud fix and branch name fix

* trying to speed up pipeline run time

* update stage

* branch name fix + sonar cloud

* sonarcloud
This commit is contained in:
souhailanoor 2022-07-28 17:23:16 -05:00 committed by GitHub
parent 94a5721d05
commit 2bdf1f5081
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
239 changed files with 226860 additions and 197 deletions

View file

@ -3,3 +3,5 @@ lattice/node_modules
lattice/build
statik/statik.go
build
testenv
bin

43
.gitignore vendored
View file

@ -23,3 +23,46 @@ outputs.json
builds/
*.tfstate.backup
.vscode
# copy of .gitignore from archived idk repo
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
vendor
.terraform
terraform.tfstate*
bin
build
testenv
.pulled
pilosa-sec-data-idk
.idea/
tags.dot
*.log
*.swp

View file

@ -3,20 +3,63 @@ include:
- template: Security/License-Scanning.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
gosec-sast:
before_script:
- export GOPRIVATE=github.com/molecula/*
- apk add openssh-client
- eval $(ssh-agent -s)
- echo "$FB_SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- git config --global --add url."ssh://git@github.com/".insteadOf "https://github.com/"
- ssh-keygen -F github.com || echo "$SSH_KNOWN_HOSTS_HASHED" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
variables:
GOVERSION: "1.17.8"
stages:
- lint
- pretest # run FB linux amd 64 because it is a dependency for idk tests
- test
- build
- post build
- integration
- gauntlet
- performance
- post build
- nonblocking
- cleanup_build
.setup_ssh:
before_script:
- export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
- export GOPRIVATE=github.com/molecula/*
## Install ssh-agent if not already installed, it is required by Docker.
## (change apt-get to yum if you use an RPM-based image)
- "command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )"
## Run ssh-agent (inside the build environment)
- eval $(ssh-agent -s)
## Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store
## We're using tr to fix line endings which makes ed25519 keys work
## without extra base64 encoding.
## https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556
- echo "$FB_SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
## Create the SSH directory and give it the right permissions
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- git config --global --get url."ssh://git@github.com/".insteadOf || git config --global --add url."ssh://git@github.com/".insteadOf "https://github.com/" || true
## Set up known_hosts so we don't get prompted when there's no
## human to answer the prompt (resulting in cryptic failures).
## ssh-keygen -F checks whether github.com is in known_hosts and
## handles HashKnownHosts appropriately.
- ssh-keygen -F github.com || echo "$SSH_KNOWN_HOSTS_HASHED" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
smoke build:
image: golang:$GOVERSION
stage: lint
@ -35,7 +78,7 @@ golangci-lint:
- if: '$CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"'
script:
- echo "Checking for issues in new code"
- golangci-lint run
- golangci-lint run -v --timeout=8m
go mod tidy:
stage: lint
@ -46,8 +89,10 @@ go mod tidy:
- go mod tidy
- git diff --exit-code -- go.mod go.sum
### lattice, build for linux amd64 and build amd fb container are needed for IDK unit testing
### running them at an earlier stage allows for decreasing run time.
build lattice:
stage: test
stage: pretest
image: node:14
variables:
CI: "false"
@ -66,6 +111,41 @@ build lattice:
paths:
- lattice.tar.gz
build for linux amd64:
stage: pretest
image: golang:$GOVERSION
rules:
- if: '$CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"'
script:
- rm -r lattice
- tar -xvf lattice.tar.gz
- go get -v -u github.com/rakyll/statik
- /go/bin/statik -src=lattice
- GOOS="linux" GOARCH="amd64" make build FLAGS="-o featurebase_linux_amd64"
- GOOS="linux" GOARCH="amd64" go build -o roaring-migrate_linux_amd64 ./cmd/roaring-migrate
artifacts:
paths:
- featurebase_linux_amd64
- roaring-migrate_linux_amd64
needs:
- job: build lattice
build amd container fb:
stage: pretest
tags:
- shell
rules:
- if: '$CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"'
before_script:
- echo "${DOCKER_DEPLOY_TOKEN}" | docker login -u ${DOCKER_DEPLOY_USER} --password-stdin ${CI_REGISTRY}
script:
- tag=${CI_REGISTRY_IMAGE}/featurebase:linux-amd64-${CI_COMMIT_REF_SLUG}
- docker build --build-arg GO_VERSION=$GOVERSION --build-arg ARCH=amd64 -t $tag -f .gitlab/Dockerfile .
- docker push $tag
- echo Created docker featurebase image with tag "$tag"
needs:
- job: build for linux amd64
run jest tests:
stage: test
image: node:14
@ -90,7 +170,7 @@ run go tests:
retry: 1
script:
- echo "Running featurebase unit tests..."
- go test -timeout=30m ./...
- go test -v -timeout=30m $(go list ./... | grep -Ev idk)
tags:
- aws
@ -103,7 +183,7 @@ run go tests race:
needs: ["smoke build"] # we do block on smoke build though bc it's pretty dumb to test stuff if it doesn't build
script:
- echo "Running featurebase race tests..."
- go test -race -v -timeout=90m ./...
- go test -race -v -timeout=90m $(go list ./... | grep -Ev idk)
tags:
- aws
@ -114,7 +194,7 @@ run go tests shardwidth22:
- if: '$CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"'
script:
- echo "Running featurebase shardwidth22 tests..."
- go test -timeout=30m -tags=shardwidth22 ./...
- go test -timeout=30m -tags=shardwidth22 -v $(go list ./... | grep -Ev idk)
tags:
- aws
@ -129,8 +209,8 @@ run go tests future:
retry: 1
script:
- echo "Running featurebase unit tests..."
- PKG_LIST=$(go list ./... | grep -Ev 'internal/clustertests|simulacraData' | paste -s -d, -)
- go test -timeout=30m -json -coverprofile=coverage.out -covermode=atomic -coverpkg=${PKG_LIST} ./... | tee test-report.out
- PKG_LIST=$(go list ./... | grep -Ev 'internal/clustertests|simulacraData' | grep -Ev 'idk' | paste -s -d, -)
- go test -timeout=30m -json -coverprofile=coverage.out -covermode=atomic -coverpkg=${PKG_LIST} $(go list ./... | grep -Ev idk) | tee test-report.out
artifacts:
paths:
- coverage.out
@ -146,8 +226,8 @@ run go tests future plg:
retry: 1
script:
- echo "Running featurebase plg-specific unit tests..."
- PKG_LIST=$(go list ./... | grep -Ev 'internal/clustertests|simulacraData' | paste -s -d, -)
- go test -tags=plg -timeout=30m -coverprofile=coverage-plg.out -covermode=atomic -coverpkg=${PKG_LIST} ./... | tee test-report-plg.out
- PKG_LIST=$(go list ./... | grep -Ev 'internal/clustertests|simulacraData' | grep -Ev 'idk' | paste -s -d, -)
- go test -tags=plg -timeout=30m -coverprofile=coverage-plg.out -covermode=atomic -coverpkg=${PKG_LIST} $(go list ./... | grep -Ev idk) | tee test-report-plg.out
artifacts:
paths:
- coverage-plg.out
@ -155,6 +235,127 @@ run go tests future plg:
tags:
- aws
# idk tests
run go tests idk race:
extends:
- .setup_ssh
variables:
USERNAME: fb-idk-access
PROJECT: race_${CI_CONCURRENT_ID}
stage: test
retry: 1
rules:
- if: '$CI_COMMIT_TAG == null && ($CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web")'
script:
- echo "Running test-all-race"
- cd ./idk/
- command -v certstrap >/dev/null || go install github.com/square/certstrap@latest
- echo $PROJECT
- echo $DOCKER_PASSWORD | docker login registry.gitlab.com --username "$USERNAME" --password-stdin
- BRANCH_NAME=${CI_COMMIT_BRANCH} make test-all-race
after_script:
- make save-pilosa-logs
- make shutdown
artifacts:
paths:
- ./idk/testdata/*_coverage.out
- ./idk/testdata/*_report.out
tags:
- shell
- aws
needs:
- job: build amd container fb
run go tests idk shard transactional:
extends:
- .setup_ssh
variables:
IDK_DEFAULT_SHARD_TRANSACTIONAL: 1
USERNAME: fb-idk-access
PROJECT: shardttrans_${CI_CONCURRENT_ID}
stage: test
retry: 1
rules:
- if: '$CI_COMMIT_TAG == null && ($CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web")'
script:
- echo "Running shard transactional tests"
- cd ./idk/
- command -v certstrap >/dev/null || go install github.com/square/certstrap@latest
- echo $PROJECT
- echo $DOCKER_PASSWORD | docker login registry.gitlab.com --username "$USERNAME" --password-stdin
- BRANCH_NAME=${CI_COMMIT_BRANCH} make test-all
after_script:
- make save-pilosa-logs
- make shutdown
artifacts:
paths:
- ./idk/testdata/*_coverage.out
- ./idk/testdata/*_report.out
- ./idk/testdata/*_logs.txt
tags:
- shell
- aws
needs:
- job: build amd container fb
run go tests idk 533:
extends:
- .setup_ssh
variables:
USERNAME: fb-idk-access
PROJECT: test533_${CI_CONCURRENT_ID}
stage: test
retry: 1
script:
- echo "Running confluent 5.3.3 test-all"
- cd ./idk/
- command -v certstrap >/dev/null || go install github.com/square/certstrap@latest
- echo $DOCKER_PASSWORD | docker login registry.gitlab.com --username "$USERNAME" --password-stdin
- CONFLUENT_VERSION=5.3.3 BRANCH_NAME=${CI_COMMIT_BRANCH} make test-all
after_script:
- make save-pilosa-logs
- make shutdown
rules:
- if: '$CI_COMMIT_TAG == null && ($CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web")'
tags:
- shell
- aws
artifacts:
paths:
- ./idk/testdata/*_coverage.out
- ./idk/testdata/*_report.out
needs:
- job: build amd container fb
run go tests idk sasl:
extends:
- .setup_ssh
variables:
USERNAME: fb-idk-access
PROJECT: sasl_${CI_CONCURRENT_ID}
stage: test
retry: 1
script:
- echo "Running test-all-kafka-sasl"
- cd ./idk/
- command -v certstrap >/dev/null || go install github.com/square/certstrap@latest
- echo $DOCKER_PASSWORD | docker login registry.gitlab.com --username "$USERNAME" --password-stdin
- BRANCH_NAME=${CI_COMMIT_BRANCH} make test-all-kafka-sasl
after_script:
- make save-pilosa-logs
- make shutdown
rules:
- if: '$CI_COMMIT_TAG == null && ($CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web")'
tags:
- shell
- aws
artifacts:
paths:
- ./idk/testdata/*_coverage.out
- ./idk/testdata/*_report.out
needs:
- job: build amd container fb
upload to sonarcloud:
stage: integration
image: sonarsource/sonar-scanner-cli:4.6
@ -164,28 +365,17 @@ upload to sonarcloud:
- if: '$CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"'
script:
- sonar-scanner -Dsonar.projectKey=molecula_featurebase -Dsonar.organization=molecula -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.go.coverage.reportPaths=coverage*.out,results/coverage*out -Dsonar.go.tests.reportPaths=test-report*.out -Dsonar.javascript.lcov.reportPaths=lattice/coverage/lcov.info
- cd ./idk
- ls ./testdata
- sonar-scanner -Dsonar.projectKey=molecula_featurebase -Dsonar.organization=molecula -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.go.coverage.reportPaths=testdata/*coverage.out -Dsonar.go.tests.reportPaths=testdata/*report.out -Dsonar.coverage.exclusions=**/*_test.go -Dsonar.cpd.exclusions=**/*_test.go
needs:
- job: run go tests future plg
- job: run go tests future
- job: run jest tests
- job: clustertests
build for linux amd64:
stage: build
image: golang:$GOVERSION
rules:
- if: '$CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"'
script:
- rm -r lattice
- tar -xvf lattice.tar.gz
- go get -v -u github.com/rakyll/statik
- /go/bin/statik -src=lattice
- GOOS="linux" GOARCH="amd64" make build FLAGS="-o featurebase_linux_amd64"
- GOOS="linux" GOARCH="amd64" go build -o roaring-migrate_linux_amd64 ./cmd/roaring-migrate
artifacts:
paths:
- featurebase_linux_amd64
- roaring-migrate_linux_amd64
- job: run go tests idk race
- job: run go tests idk sasl
- job: run go tests idk 533
build for linux arm64:
stage: build
@ -377,22 +567,6 @@ package plg for linux arm64:
# end PLG
build amd container fb:
stage: build
needs:
- "build for linux amd64"
tags:
- shell
rules:
- if: '$CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"'
before_script:
- echo "${DOCKER_DEPLOY_TOKEN}" | docker login -u ${DOCKER_DEPLOY_USER} --password-stdin ${CI_REGISTRY}
script:
- tag=${CI_REGISTRY_IMAGE}/featurebase:linux-amd64-${CI_COMMIT_REF_SLUG}
- docker build --build-arg GO_VERSION=$GOVERSION --build-arg ARCH=amd64 -t $tag -f .gitlab/Dockerfile .
- docker push $tag
- echo Created docker featurebase image with tag "$tag"
build arm container fb:
stage: build
needs:
@ -409,6 +583,128 @@ build arm container fb:
- docker push $tag
- echo Created docker featurebase image with tag "$tag"
### start idk builds ###
# building them all serially because otherwise you get container name conflicts.
idk build_all:
stage: build
variables:
BUILD_NAME: build_${CI_COMMIT_SHA}_${CI_CONCURRENT_ID}
extends:
- .setup_ssh
tags:
- shell
rules:
- if: '$CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"'
script:
- cd ./idk/
- make docker-build GOOS="linux" GOARCH="amd64" BUILD_CGO=1
- make docker-build GOOS="darwin" GOARCH="amd64"
- make docker-build GOOS="darwin" GOARCH="arm64"
artifacts:
paths:
- ./idk/build/*
idk build_linux_arm64:
stage: build
extends:
- .setup_ssh
tags:
- shell-arm64
rules:
- if: '$CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"'
script:
- cd ./idk/
- make docker-build GOOS="linux" GOARCH="arm64" BUILD_CGO=1 BUILD_NAME="linux-arm64"
artifacts:
paths:
- ./idk/build/*
# building them all serially because otherwise you get container name conflicts.
# only do containers on default branch
idk package_docker_all:
stage: build
extends:
- .setup_ssh
tags:
- shell
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && ($CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web")'
script:
- cd ./idk/
- make docker GOOS="linux" GOARCH="amd64"
- make docker GOOS="linux" GOARCH="arm64"
- make docker GOOS="darwin" GOARCH="amd64"
- make docker GOOS="darwin" GOARCH="arm64"
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- make docker-tag-push GOOS="linux" GOARCH="amd64"
- make docker-tag-push GOOS="linux" GOARCH="arm64"
- make docker-tag-push GOOS="darwin" GOARCH="amd64"
- make docker-tag-push GOOS="darwin" GOARCH="arm64"
needs:
- job: idk build_all
- job: idk build_linux_arm64
idk s3 dump:
stage: post build
extends:
- .setup_ssh
allow_failure: false
variables:
PROFILE: "service-fb-ci"
AWS_SSH_PRIVATE_KEY: $AWS_FBCI_SSH_KEY
AWS_ACCESS_KEY_ID: $AWS_FBCI_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY: $AWS_FBCI_SECRET_ACCESS_KEY
tags:
- shell
rules:
- if: '$CI_COMMIT_TAG == null && ($CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web")'
script:
- aws configure set aws_access_key_id $AWS_FBCI_ACCESS_KEY_ID
- aws configure set aws_secret_access_key $AWS_FBCI_SECRET_ACCESS_KEY
- aws configure set region "us-east-2"
- aws configure set aws_profile $PROFILE
- aws s3 cp ./idk/build/ s3://molecula-artifact-storage/idk/${CI_COMMIT_BRANCH}/${CI_COMMIT_SHORT_SHA}/ --recursive
- aws s3 cp ./idk/build/ s3://molecula-artifact-storage/idk/${CI_COMMIT_BRANCH}/_latest/ --recursive
needs:
- job: idk build_all
- job: idk build_linux_arm64
idk s3 dump tag:
stage: post build
extends:
- .setup_ssh
variables:
PROFILE: "service-fb-ci"
AWS_SSH_PRIVATE_KEY: $AWS_FBCI_SSH_KEY
AWS_ACCESS_KEY_ID: $AWS_FBCI_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY: $AWS_FBCI_SECRET_ACCESS_KEY
LOCATION: molecula-artifact-storage/idk/_tags
tags:
- shell
rules:
- if: '$CI_COMMIT_TAG != null && ($CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web")'
script:
- aws configure set aws_access_key_id $AWS_FBCI_ACCESS_KEY_ID
- aws configure set aws_secret_access_key $AWS_FBCI_SECRET_ACCESS_KEY
- aws configure set region "us-east-2"
- aws configure set aws_profile $PROFILE
- |
for goos in "darwin" "linux"; do
for goarch in "amd64" "arm64"; do
dir=idk-${CI_COMMIT_TAG}-${goos}-${goarch}
echo "Directory ${dir}"
mkdir ${dir}
mv ./idk/build/idk-${goos}-${goarch}/molecula-consumer-* ${dir}/
tar cvzf ${dir}.tar.gz ${dir}
aws s3 cp ${dir} s3://${LOCATION}/${CI_COMMIT_TAG}/${dir}/ --recursive
aws s3 cp ${dir}.tar.gz s3://${LOCATION}/${CI_COMMIT_TAG}/
done
done
needs:
- job: idk build_all
- job: idk build_linux_arm64
### end idk builds ###
# clustertests doesn't run in docker, and requires several things to be set up on the runner to work:
# 1. Install Go, make sure it's on the path
# 2. Make sure "make" is installed
@ -502,16 +798,18 @@ smoke test auth:
- export PATH=$PATH:/usr/local/go/bin
- TF_VAR_cluster_prefix="pipeline-$CI_PIPELINE_ID-auth-smoke-$CI_JOB_ID"
- echo "Cluster Prefix --> $TF_VAR_cluster_prefix"
# download datagen for FB-1270 repro test. TODO replace w/ locally built datagen once we merge IDK into FB
- aws s3 cp s3://molecula-artifact-storage/idk/master/_latest/idk-linux-arm64/datagen ./datagen_linux_arm64
- chmod +x ./datagen_linux_arm64
# download datagen for FB-1270 repro test.
- aws s3 cp s3://molecula-artifact-storage/idk/${CI_COMMIT_BRANCH}/_latest/idk-linux-arm64/datagen ./datagen_linux_arm64
- aws s3 cp s3://molecula-artifact-storage/featurebase/${CI_COMMIT_BRANCH}/_latest/featurebase_linux_arm64 ./
- chmod +x ./datagen_linux_arm64 ./featurebase_linux_arm64
script:
- ./qa/scripts/auth-smoke/setup.sh
- ./qa/scripts/auth-smoke/setup.sh $CI_COMMIT_BRANCH
- ./qa/scripts/auth-smoke/test.sh
after_script:
- ./qa/scripts/auth-smoke/teardown.sh
needs:
- job: build for linux arm64
- job: s3 dump
- job: idk s3 dump
artifacts:
when: always
paths:
@ -559,17 +857,19 @@ smoke test:
- export PATH=$PATH:/usr/local/go/bin
- TF_VAR_cluster_prefix="pipeline-$CI_PIPELINE_ID-smoke-$CI_JOB_ID"
- echo "Cluster Prefix --> $TF_VAR_cluster_prefix"
# download datagen for FB-1270 repro test. TODO replace w/ locally built datagen once we merge IDK into FB
- aws s3 cp s3://molecula-artifact-storage/idk/master/_latest/idk-linux-arm64/datagen ./datagen_linux_arm64
- chmod +x ./datagen_linux_arm64
# download datagen for FB-1270 repro test.
- aws s3 cp s3://molecula-artifact-storage/idk/${CI_COMMIT_BRANCH}/_latest/idk-linux-arm64/datagen ./datagen_linux_arm64
- aws s3 cp s3://molecula-artifact-storage/featurebase/${CI_COMMIT_BRANCH}/_latest/featurebase_linux_arm64 ./
- chmod +x ./datagen_linux_arm64 ./featurebase_linux_arm64
script:
- ./qa/scripts/setupSmokeTest.sh
- ./qa/scripts/setupSmokeTest.sh $CI_COMMIT_BRANCH
- ./qa/scripts/testSmokeTest.sh
- ./qa/scripts/bug_repro_tests.sh
after_script:
- ./qa/scripts/teardownSmokeTest.sh
needs:
- job: build for linux arm64
- job: s3 dump
- job: idk s3 dump
artifacts:
when: always
paths:
@ -628,19 +928,19 @@ tremor-delete-test:
script:
- echo "Start CloudFormation deploy..."
- >
aws cloudformation deploy
--stack-name $CF_STACK_NAME
--template-file ./qa/cft/FeatureBaseClusterCFTTremor.yaml
--parameter-overrides
FBInstanceCount=$FB_INSTANCE_COUNT
EBSVolumeSize=1200
TestName=$CI_JOB_NAME
TestID=$CI_JOB_ID
Name=$CF_VAR_cluster_prefix
Prefix=$CF_VAR_cluster_prefix
TestCommitSHA=$CI_COMMIT_SHA
--capabilities CAPABILITY_NAMED_IAM
--profile $PROFILE
aws cloudformation deploy
--stack-name $CF_STACK_NAME
--template-file ./qa/cft/FeatureBaseClusterCFTTremor.yaml
--parameter-overrides
FBInstanceCount=$FB_INSTANCE_COUNT
EBSVolumeSize=1200
TestName=$CI_JOB_NAME
TestID=$CI_JOB_ID
Name=$CF_VAR_cluster_prefix
Prefix=$CF_VAR_cluster_prefix
TestCommitSHA=$CI_COMMIT_SHA
--capabilities CAPABILITY_NAMED_IAM
--profile $PROFILE
- echo "CloudFormation deploy done."
- echo "Start configuring and start featurebase..."
- ./qa/scripts/setupTremorDeleteCFT.sh $CF_STACK_NAME $PROFILE $REPLICA_COUNT $FB_INSTANCE_COUNT $CI_COMMIT_BRANCH
@ -660,6 +960,9 @@ tremor-delete-test:
- echo "Deleting stack after test, stack name = ${CF_STACK_NAME}"
- aws cloudformation delete-stack --stack-name $CF_STACK_NAME --profile $PROFILE --retain-resources DeploymentEC2Role
- echo "Delete stack complete!"
needs:
- job: s3 dump
- job: idk s3 dump
samsung-gauntlet:
stage: gauntlet
@ -710,13 +1013,18 @@ samsung-gauntlet:
- echo "Cluster Prefix --> $TF_VAR_cluster_prefix"
- export INSTANCE_ID=$(curl --silent --fail "http://169.254.169.254/latest/meta-data/instance-id" | tee instance_id)
- aws autoscaling set-instance-protection --instance-ids "$INSTANCE_ID" --auto-scaling-group-name $ASG_NAME --protected-from-scale-in --profile $INFRA_PROFILE
- aws s3 cp s3://molecula-artifact-storage/featurebase/${CI_COMMIT_BRANCH}/_latest/featurebase_linux_arm64 ./
- chmod +x ./featurebase_linux_arm64
script:
- ./qa/scripts/setupSamsungGauntlet.sh
- ./qa/scripts/setupSamsungGauntlet.sh $CI_COMMIT_BRANCH
- ./qa/scripts/testSamsungGauntlet.sh
after_script:
- ./qa/scripts/teardownSamsungGauntlet.sh || true # leaving dangling resources is better than dangling ASG instances that can't be terminated
- export INSTANCE_ID=$(cat instance_id)
- aws autoscaling set-instance-protection --instance-ids "$INSTANCE_ID" --auto-scaling-group-name $ASG_NAME --no-protected-from-scale-in --profile $INFRA_PROFILE
needs:
- job: s3 dump
- job: idk s3 dump
backup-restore-gauntlet:
stage: gauntlet
@ -767,15 +1075,18 @@ backup-restore-gauntlet:
- echo "Cluster Prefix --> $TF_VAR_cluster_prefix"
- export INSTANCE_ID=$(curl --silent --fail "http://169.254.169.254/latest/meta-data/instance-id" | tee instance_id)
- aws autoscaling set-instance-protection --instance-ids "$INSTANCE_ID" --auto-scaling-group-name $ASG_NAME --protected-from-scale-in --profile $INFRA_PROFILE
- aws s3 cp s3://molecula-artifact-storage/featurebase/${CI_COMMIT_BRANCH}/_latest/featurebase_linux_arm64 ./
- chmod +x ./featurebase_linux_arm64
script:
- ./qa/scripts/setupBackupRestoreGauntlet.sh
- ./qa/scripts/testBackupRestoreGauntlet.sh
- ./qa/scripts/setupBackupRestoreGauntlet.sh $CI_COMMIT_BRANCH
- ./qa/scripts/testBackupRestoreGauntlet.sh $CI_COMMIT_BRANCH
after_script:
- ./qa/scripts/teardownBackupRestoreGauntlet.sh || true # leaving dangling resources is better than dangling ASG instances that can't be terminated
- export INSTANCE_ID=$(cat instance_id)
- aws autoscaling set-instance-protection --instance-ids "$INSTANCE_ID" --auto-scaling-group-name $ASG_NAME --no-protected-from-scale-in --profile $INFRA_PROFILE
needs:
- build for linux arm64
- job: s3 dump
- job: idk s3 dump
s3 dump:
stage: post build
@ -808,7 +1119,7 @@ s3 dump:
- aws s3 cp featurebase_darwin_arm64 s3://molecula-artifact-storage/featurebase/${CI_COMMIT_BRANCH}/${CI_COMMIT_SHORT_SHA}/featurebase_darwin_arm64
- aws s3 cp featurebase_darwin_arm64 s3://molecula-artifact-storage/featurebase/${CI_COMMIT_BRANCH}/_latest/featurebase_darwin_arm64
- aws s3 cp roaring-migrate_darwin_arm64 s3://molecula-artifact-storage/featurebase/${CI_COMMIT_BRANCH}/${CI_COMMIT_SHORT_SHA}/roaring-migrate_darwin_arm64
- aws s3 cp roaring-migrate_darwin_arm64 s3://molecula-artifact-storage/featurebase/${CI_COMMIT_BRANCH}/_latest/roaring-migrate_darwin_arm64
- aws s3 cp roaring-migrate_darwin_arm64 s3://molecula-artifact-storage/featurebase/${CI_COMMIT_BRANCH}/_latest/roaring-migrate_darwin_arm64
- aws s3 cp single_node_featurebase_linux_amd64 s3://molecula-artifact-storage/single_node_featurebase/${CI_COMMIT_BRANCH}/${CI_COMMIT_SHORT_SHA}/single_node_featurebase_linux_amd64
- aws s3 cp single_node_featurebase_linux_amd64 s3://molecula-artifact-storage/single_node_featurebase/${CI_COMMIT_BRANCH}/_latest/single_node_featurebase_linux_amd64
- aws s3 cp single_node_featurebase_linux_arm64 s3://molecula-artifact-storage/single_node_featurebase/${CI_COMMIT_BRANCH}/${CI_COMMIT_SHORT_SHA}/single_node_featurebase_linux_arm64
@ -816,7 +1127,7 @@ s3 dump:
- aws s3 cp single_node_featurebase_darwin_amd64 s3://molecula-artifact-storage/single_node_featurebase/${CI_COMMIT_BRANCH}/${CI_COMMIT_SHORT_SHA}/single_node_featurebase_darwin_amd64
- aws s3 cp single_node_featurebase_darwin_amd64 s3://molecula-artifact-storage/single_node_featurebase/${CI_COMMIT_BRANCH}/_latest/single_node_featurebase_darwin_amd64
- aws s3 cp single_node_featurebase_darwin_arm64 s3://molecula-artifact-storage/single_node_featurebase/${CI_COMMIT_BRANCH}/${CI_COMMIT_SHORT_SHA}/single_node_featurebase_darwin_arm64
- aws s3 cp single_node_featurebase_darwin_arm64 s3://molecula-artifact-storage/single_node_featurebase/${CI_COMMIT_BRANCH}/_latest/single_node_featurebase_darwin_arm64
- aws s3 cp single_node_featurebase_darwin_arm64 s3://molecula-artifact-storage/single_node_featurebase/${CI_COMMIT_BRANCH}/_latest/single_node_featurebase_darwin_arm64
needs:
- job: build for darwin amd64
- job: build for darwin arm64
@ -889,15 +1200,18 @@ perf_delete:
- echo "Cluster Prefix --> $TF_VAR_cluster_prefix"
- export INSTANCE_ID=$(curl --silent --fail "http://169.254.169.254/latest/meta-data/instance-id" | tee instance_id)
- aws autoscaling set-instance-protection --instance-ids "$INSTANCE_ID" --auto-scaling-group-name $ASG_NAME --protected-from-scale-in --profile $INFRA_PROFILE
- aws s3 cp s3://molecula-artifact-storage/featurebase/${CI_COMMIT_BRANCH}/_latest/featurebase_linux_arm64 ./
- chmod +x ./featurebase_linux_arm64
script:
- ./qa/scripts/perf/delete/deleteSetup.sh
- ./qa/scripts/perf/delete/deleteTest.sh
- ./qa/scripts/perf/delete/deleteSetup.sh $CI_COMMIT_BRANCH
- ./qa/scripts/perf/delete/deleteTest.sh $CI_COMMIT_BRANCH
after_script:
- ./qa/scripts/perf/delete/deleteTeardown.sh || true
- export INSTANCE_ID=$(cat instance_id)
- aws autoscaling set-instance-protection --instance-ids "$INSTANCE_ID" --auto-scaling-group-name $ASG_NAME --no-protected-from-scale-in --profile $INFRA_PROFILE
needs:
- job: build for linux arm64
- job: s3 dump
- job: idk s3 dump
s3 dump tag:
stage: post build
@ -992,4 +1306,6 @@ cleanup_build_job:
- aws configure set aws_profile $FBCI_PROFILE
- ./qa/scripts/gitlabCleanupBuild.sh
when: always
needs:
- job: smoke test
- job: smoke test auth

View file

@ -1,6 +1,6 @@
variables:
GOVERSION: "1.17.8"
stages:
- performance
@ -34,7 +34,7 @@ perf_able:
- aws configure set region "us-east-2" --profile $INFRA_PROFILE
- echo $AWS_FBCI_SSH_KEY > gitlab-featurebase-ci.pem
- chmod 400 gitlab-featurebase-ci.pem
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
- "which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )"
- eval $(ssh-agent -s)
- mkdir -p ~/.ssh
- echo $AWS_FBCI_SSH_KEY > /root/.ssh/gitlab-featurebase-ci.pem
@ -50,8 +50,10 @@ perf_able:
- echo "Cluster Prefix --> $TF_VAR_cluster_prefix"
- export INSTANCE_ID=$(curl --silent --fail "http://169.254.169.254/latest/meta-data/instance-id" | tee instance_id)
- aws autoscaling set-instance-protection --instance-ids "$INSTANCE_ID" --auto-scaling-group-name $ASG_NAME --protected-from-scale-in --profile $INFRA_PROFILE
- aws s3 cp s3://molecula-artifact-storage/featurebase/${CI_COMMIT_BRANCH}/_latest/featurebase_linux_arm64 ./
- chmod +x ./featurebase_linux_arm64
script:
- ./qa/scripts/perf/able/ableSetup.sh
- ./qa/scripts/perf/able/ableSetup.sh $CI_COMMIT_BRANCH
- ./qa/scripts/perf/able/ableTest.sh
after_script:
- ./qa/scripts/perf/able/ableTeardown.sh || true

89
go.mod
View file

@ -1,20 +1,30 @@
module github.com/molecula/featurebase/v3
replace github.com/go-avro/avro => github.com/pilosa/avro v0.0.0-20200626214113-bc1bf9fd41c1
require (
github.com/CAFxX/gcnotifier v0.0.0-20220409005548-0153238b886a
github.com/DataDog/datadog-go v4.8.3+incompatible
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
github.com/alexbrainman/odbc v0.0.0-20211220213544-9c9a2e61c5e2
github.com/aws/aws-sdk-go v1.42.39
github.com/beevik/ntp v0.3.0
github.com/benbjohnson/immutable v0.3.0
github.com/buger/jsonparser v1.1.1
github.com/cespare/xxhash v1.1.0
github.com/confluentinc/confluent-kafka-go v1.9.0
github.com/davecgh/go-spew v1.1.1
github.com/felixge/fgprof v0.9.1
github.com/denisenkom/go-mssqldb v0.11.0
github.com/felixge/fgprof v0.9.2
github.com/getsentry/sentry-go v0.13.0
github.com/glycerine/vprint v0.0.0-20200730000117-76cea49a68ea
github.com/go-avro/avro v0.0.0-20171219232920-444163702c11
github.com/go-sql-driver/mysql v1.6.0
github.com/go-test/deep v1.0.7
github.com/gogo/protobuf v1.3.2
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.2
github.com/google/go-cmp v0.5.7
github.com/google/go-cmp v0.5.8
github.com/gorilla/handlers v1.3.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/securecookie v1.1.1
@ -25,18 +35,21 @@ require (
github.com/opentracing/opentracing-go v1.2.0
github.com/pelletier/go-toml v1.9.5
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.11.1
github.com/prometheus/client_golang v1.12.2
github.com/prometheus/client_model v0.2.0
github.com/prometheus/prom2json v1.3.0
github.com/prometheus/prom2json v1.3.1
github.com/rakyll/statik v0.1.7
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/satori/go.uuid v1.2.0
github.com/shirou/gopsutil/v3 v3.21.9
github.com/spf13/cobra v1.1.3
github.com/segmentio/kafka-go v0.4.29
github.com/shirou/gopsutil/v3 v3.22.5
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.7.0
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.7.1
github.com/uber/jaeger-client-go v2.25.0+incompatible
github.com/zeebo/blake3 v0.1.1
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
github.com/zeebo/blake3 v0.2.3
go.etcd.io/bbolt v1.3.6
go.etcd.io/etcd v3.3.27+incompatible
go.etcd.io/etcd/api/v3 v3.5.4
@ -45,22 +58,28 @@ require (
go.etcd.io/etcd/server/v3 v3.5.4
golang.org/x/exp v0.0.0-20201008143054-e3b2a7f2fdc7
golang.org/x/mod v0.5.1
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
google.golang.org/grpc v1.38.0
gopkg.in/DataDog/dd-trace-go.v1 v1.38.1
gopkg.in/avro.v0 v0.0.0-20171217001914-a730b5802183 // indirect
gopkg.in/yaml.v2 v2.4.0
modernc.org/mathutil v1.0.0
modernc.org/strutil v1.0.0
sigs.k8s.io/yaml v1.2.0
vitess.io/vitess v3.0.0-rc.3.0.20190602171040-12bfde34629c+incompatible
)
require (
github.com/jaffee/commandeer v0.5.0
github.com/linkedin/goavro/v2 v2.11.1
)
require (
github.com/DataDog/datadog-go/v5 v5.1.0 // indirect
github.com/DataDog/gostackparse v0.5.0 // indirect
github.com/HdrHistogram/hdrhistogram-go v1.1.0 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
@ -70,10 +89,12 @@ require (
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/pprof v0.0.0-20210423192551-a2663126120b // indirect
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
@ -83,30 +104,35 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.3.0 // indirect
github.com/json-iterator/go v1.1.11 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.15.1 // indirect
github.com/magiconair/properties v1.8.1 // indirect
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/common v0.33.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/rs/cors v1.8.2 // indirect
github.com/sirupsen/logrus v1.7.0 // indirect
github.com/soheilhy/cmux v0.1.5 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.3.0 // indirect
github.com/spf13/jwalterweatherman v1.0.0 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/stretchr/objx v0.2.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/tklauser/go-sysconf v0.3.9 // indirect
github.com/tklauser/numcpus v0.3.0 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
github.com/uber/jaeger-lib v2.4.0+incompatible // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.etcd.io/etcd/client/v2 v2.305.4 // indirect
go.etcd.io/etcd/pkg/v3 v3.5.4 // indirect
go.etcd.io/etcd/raft/v3 v3.5.4 // indirect
@ -127,16 +153,13 @@ require (
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.6 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/ini.v1 v1.51.1 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
nhooyr.io/websocket v1.8.6 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)
go 1.17

260
go.sum
View file

@ -10,8 +10,15 @@ cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0 h1:EpMNVUorLiZIELdMZbCYX/ByTFCdoYopYAGxaGVz9ms=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -30,6 +37,7 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
@ -60,8 +68,8 @@ github.com/DataDog/gostackparse v0.5.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZ
github.com/DataDog/sketches-go v1.0.0 h1:chm5KSXO7kO+ywGWJ0Zs6tdmWU8PBXSbywFVciL6BG4=
github.com/DataDog/sketches-go v1.0.0/go.mod h1:O+XkJHWk9w4hDwY2ZUDU31ZC9sNYlYo8DiFsxjYeo1k=
github.com/DataDog/zstd v1.3.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/HdrHistogram/hdrhistogram-go v1.1.0 h1:6dpdDPTRoo78HxAJ6T1HfMiKSnqhgRRqzCuPshRkQ7I=
github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
@ -78,8 +86,6 @@ github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqR
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/sarama v1.22.0/go.mod h1:lm3THZ8reqBDBQKQyb5HB3sY1lKp3grEbQ81aWSgPp4=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
@ -89,6 +95,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alexbrainman/odbc v0.0.0-20211220213544-9c9a2e61c5e2 h1:090cWAt7zsbdvRegKCBVwcCTghjxhUh1PK2KNSq82vw=
github.com/alexbrainman/odbc v0.0.0-20211220213544-9c9a2e61c5e2/go.mod h1:c5eyz5amZqTKvY3ipqerFO/74a/8CYmXOahSr40c+Ww=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@ -103,6 +111,8 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ
github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
github.com/aws/aws-sdk-go v1.42.39 h1:6Lso73VoCI8Zmv3zAMv4BNg2gHAKNOlbLv1s/ew90SI=
github.com/aws/aws-sdk-go v1.42.39/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aws/aws-sdk-go-v2 v1.0.0/go.mod h1:smfAbmpW+tcRVuNUjo3MOArSZmW72t62rkCzc2i0TWM=
github.com/aws/aws-sdk-go-v2/config v1.0.0/go.mod h1:WysE/OpUgE37tjtmtJd8GXgT8s1euilE5XtUkRNUQ1w=
@ -127,6 +137,7 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
@ -154,6 +165,7 @@ github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
@ -166,6 +178,8 @@ github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u9
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/confluentinc/confluent-kafka-go v1.4.0/go.mod h1:u2zNLny2xq+5rWeTQjFHbDzzNuba4P1vo31r9r4uAdg=
github.com/confluentinc/confluent-kafka-go v1.9.0 h1:d1k62oAuQVxgdMdiDQnpkABbtIWTBwXHpDcyGQUw5QQ=
github.com/confluentinc/confluent-kafka-go v1.9.0/go.mod h1:WDFs+KlhHITEoCzEfHSNgj5aP7vjajyYbZpvTEGs1sE=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ=
@ -193,6 +207,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/denisenkom/go-mssqldb v0.11.0 h1:9rHa233rhdOyrz2GcP9NM+gi2psgJZ4GWDpL/7ND8HI=
github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I=
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE=
@ -220,6 +235,7 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@ -230,8 +246,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/fgprof v0.9.1 h1:E6FUJ2Mlv043ipLOCFqo8+cHo9MhQ203E2cdEK/isEs=
github.com/felixge/fgprof v0.9.1/go.mod h1:7/HK6JFtFaARhIljgP2IV8rJLIoHDoOYoUphsnGvqxE=
github.com/felixge/fgprof v0.9.2 h1:tAMHtWMyl6E0BimjVbFt7fieU6FpjttsZN7j0wT5blc=
github.com/felixge/fgprof v0.9.2/go.mod h1:+VNi+ZXtHIQ6wIw6bUT8nXQRefQflWECoFyRealT5sg=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
@ -257,6 +273,8 @@ github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjX
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/glycerine/vprint v0.0.0-20200730000117-76cea49a68ea h1:Uiuhuh77mImdrAMjPfw2V8tWw4AF6r9dxbNkECo23SA=
github.com/glycerine/vprint v0.0.0-20200730000117-76cea49a68ea/go.mod h1:q7RHAiHHxYrXtGEkX14OuACg+cHODdKnVvTgpBnOzHk=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-chi/chi v1.5.0/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k=
@ -270,14 +288,17 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
@ -297,6 +318,7 @@ github.com/go-redis/redis/v7 v7.1.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRf
github.com/go-redis/redis/v8 v8.0.0/go.mod h1:isLoQT/NFSP7V67lyvM9GmdvLdyZ7pEhsXvvyQtnQTo=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
@ -349,6 +371,7 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
@ -365,6 +388,8 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -388,6 +413,7 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.7.0/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
@ -403,24 +429,34 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200615235658-03e1cf38a040/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20210423192551-a2663126120b h1:l2YRhr+YLzmSp7KJMswRVk/lO5SwoFIcCLzJsVj+YPc=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210423192551-a2663126120b/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -445,6 +481,7 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
@ -521,6 +558,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ=
@ -588,12 +626,16 @@ github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jaffee/commandeer v0.5.0 h1:241M9N+gHQmPyjIG+yy8GGcZPfzFuIyOmJHzm5ka92g=
github.com/jaffee/commandeer v0.5.0/go.mod h1:kCwfuSvZ2T0NVEr3LDSo6fDUgi0xSBnAVDdkOKTtpLQ=
github.com/jinzhu/gorm v1.9.1/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
@ -610,8 +652,9 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
@ -641,10 +684,14 @@ github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A=
github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@ -669,10 +716,15 @@ github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/linkedin/goavro/v2 v2.11.1 h1:4cuAtbDfqkKnBXp9E+tRkIJGa6W6iAjwonwt8O1f4U0=
github.com/linkedin/goavro/v2 v2.11.1/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@ -721,6 +773,7 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
@ -730,8 +783,9 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/molecula/apophenia v0.0.0-20190827192002-68b7a14a478b h1:cZADDaNYM7xn/nklO3g198JerGQjadFuA0ofxBJgK0Y=
github.com/molecula/apophenia v0.0.0-20190827192002-68b7a14a478b/go.mod h1:uXd1BiH7xLmgkhVmspdJLENv6uGWrTL/MQX2TN7Yz9s=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
@ -789,6 +843,7 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
@ -798,8 +853,12 @@ github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG
github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pilosa/avro v0.0.0-20200626214113-bc1bf9fd41c1 h1:VEiqiLmebtgmPmpMhcByL5oXeLmDZyCLx+JmCJxDlG4=
github.com/pilosa/avro v0.0.0-20200626214113-bc1bf9fd41c1/go.mod h1:evNEPnmTS6kdXAZXtqlZgYvM+FwoJ9YdGc7YbLb2RVU=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -807,10 +866,13 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
@ -818,8 +880,11 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -835,8 +900,10 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.33.0 h1:rHgav/0a6+uYgGdNt3jwz8FNSesO/Hsang3O0T9A5SE=
github.com/prometheus/common v0.33.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@ -845,16 +912,17 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/prom2json v1.3.0 h1:BlqrtbT9lLH3ZsOVhXPsHzFrApCTKRifB7gjJuypu6Y=
github.com/prometheus/prom2json v1.3.0/go.mod h1:rMN7m0ApCowcoDlypBHlkNbp5eJQf/+1isKykIP5ZnM=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/prom2json v1.3.1 h1:OogL5hsrJpLPz3jZ4LPz4sJRTtADzViCNRQoqrzUQvk=
github.com/prometheus/prom2json v1.3.1/go.mod h1:A8Oy9aiQx4wrJY9ya1i4nHOySGmkVp5EO0aU1iSJR+g=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ=
github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 h1:HQagqIiBmr8YXawX/le3+O26N+vPPC1PtjaF3mwnook=
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@ -876,10 +944,11 @@ github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/kafka-go v0.4.29 h1:4ujULpikzHG0HqKhjumDghFjy/0RRCSl/7lbriwQAH0=
github.com/segmentio/kafka-go v0.4.29/go.mod h1:m1lXeqJtIFYZayv0shM/tjrAFljvWLTprxBHd+3PnaU=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil/v3 v3.21.9 h1:Vn4MUz2uXhqLSiCbGFRc0DILbMVLAY92DSkT8bsYrHg=
github.com/shirou/gopsutil/v3 v3.21.9/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ=
github.com/shirou/gopsutil/v3 v3.22.5 h1:atX36I/IXgFiB81687vSiBI5zrMsxcIBkP9cQMJQoJA=
github.com/shirou/gopsutil/v3 v3.22.5/go.mod h1:so9G9VzeHt/hsd0YwqprnjHnfARAUktauykSbr+y2gA=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
@ -903,25 +972,30 @@ github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJ
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
@ -935,8 +1009,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
@ -957,10 +1032,10 @@ github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSR
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ=
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo=
github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs=
github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ=
github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o=
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA=
@ -970,8 +1045,9 @@ github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqri
github.com/twitchtv/twirp v8.1.1+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
github.com/uber/jaeger-client-go v2.25.0+incompatible h1:IxcNZ7WRY1Y3G4poYlx24szfsn/3LvK9QHCq9oQw8+U=
github.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.0+incompatible h1:fY7QsGQWiCt8pajv4r7JEvmATdCVaWxXbjwyYwsNaLQ=
github.com/uber/jaeger-lib v2.4.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
@ -998,7 +1074,9 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
@ -1012,14 +1090,17 @@ github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FB
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.1.1 h1:Nbsts7DdKThRHHd+YNlqiGlRqGEF2bE2eXN+xQ1hsEs=
github.com/zeebo/blake3 v0.1.1/go.mod h1:G9pM4qQwjRzF1/v7+vabMj/c5mWpGZ2Wzo3Eb4z0pb4=
github.com/zeebo/pcg v1.0.0 h1:dt+dx+HvX8g7Un32rY9XWoYnd0NmKmrIzpHF7qiTDj0=
github.com/zeebo/pcg v1.0.0/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@ -1029,10 +1110,13 @@ go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.etcd.io/etcd v3.3.27+incompatible h1:5hMrpf6REqTHV2LW2OclNpRtxI0k9ZplMemJsMSWju0=
go.etcd.io/etcd v3.3.27+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc=
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg=
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.etcd.io/etcd/client/v2 v2.305.4 h1:Dcx3/MYyfKcPNLpR4VVQUP5KgYrBeJtktBwEKkw08Ao=
go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU=
go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4=
@ -1051,6 +1135,8 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0=
go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 h1:sO4WKdPAudZGKPcpZT4MJn6JaDmpyLrMPDGGyA1SttE=
@ -1156,6 +1242,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
@ -1167,6 +1254,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
@ -1186,9 +1275,11 @@ golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -1209,26 +1300,46 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb h1:8tDJ3aechhddbdPAxpycgXHJRMLpk/Ab+aa4OgdN5/g=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1237,10 +1348,12 @@ golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1300,14 +1413,22 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201014080544-cc95f250f6bc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1317,11 +1438,12 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@ -1405,9 +1527,19 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200527183253-8e7acdbce89d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
@ -1437,15 +1569,24 @@ google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.25.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -1475,8 +1616,22 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200528110217-3d3490e7e671/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200726014623-da3ae01ef02d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
@ -1496,9 +1651,16 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
@ -1514,11 +1676,14 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/DataDog/dd-trace-go.v1 v1.38.1 h1:nAKgcpJLXRHF56cKCP3bN8gTTQmmNAZFEblbyGKhKTo=
gopkg.in/DataDog/dd-trace-go.v1 v1.38.1/go.mod h1:GBhK4yaMJ1h329ivtKAqRNe1EZ944UnZwtz5lh7CnJc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/avro.v0 v0.0.0-20171217001914-a730b5802183 h1:PGIdqvwfpMUyUP+QAlAnKTSWQ671SmYjoou2/5j7HXk=
gopkg.in/avro.v0 v0.0.0-20171217001914-a730b5802183/go.mod h1:FvqrFXt+jCsyQibeRv4xxEJBL5iG2DDW5aeJwzDiq4A=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -1531,8 +1696,9 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.51.1 h1:GyboHr4UqMiLUybYjd22ZjQIKEJEpgtLXtuGbR21Oho=
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/jinzhu/gorm.v1 v1.9.1/go.mod h1:56JJPUzbikvTVnoyP1nppSkbJ2L8sunqTBDY2fDrmFg=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=

4
idk/.cloud-env.template Normal file
View file

@ -0,0 +1,4 @@
PROFILE = <replace with local AWS profile name>
AWS_REGION = <replace with region where image should live>
ECR_URL = <replace with ECR URL for the account>
ECR_REPO = <replace with ECR repo name>

38
idk/Dockerfile Normal file
View file

@ -0,0 +1,38 @@
ARG GO_VERSION=1.17.8
FROM golang:1.17.8-stretch as builder
WORKDIR /
RUN ["apt-get","update","-y"]
RUN ["apt-get","install","-y","git","unixodbc","unixodbc-dev","netcat", "build-essential","musl-tools"]
RUN ["git", "clone", "https://github.com/edenhill/librdkafka.git"]
WORKDIR /librdkafka
RUN ["./configure", "--prefix", "/usr"]
RUN ["make"]
RUN ["make", "install"]
WORKDIR /featurebase
COPY . .
ARG MAKE_FLAGS
ARG GO_BUILD_FLAGS
WORKDIR /featurebase/idk/
RUN make build GO_BUILD_FLAGS="-mod=vendor ${GO_BUILD_FLAGS}" ${MAKE_FLAGS}
FROM ubuntu:20.04 as runner
RUN ["apt-get","update","-y"]
RUN ["apt-get","install","-y","unixodbc-dev","netcat","ca-certificates", "musl-tools"]
COPY --from=builder /idk/bin/* /usr/local/bin/
COPY /docker-sasl/ssl_keys /etc/kafka/secrets
# Verify that the linker can find everything.
FROM runner as linkcheck
RUN if [ -e /usr/local/bin/molecula-consumer-sql-odbc ] ; then ldd /usr/local/bin/molecula-consumer-sql-odbc; fi
FROM runner

7
idk/Dockerfile-fakeIDP Normal file
View file

@ -0,0 +1,7 @@
FROM golang:1.17.8
WORKDIR /
COPY fakeidp ./
RUN go build .
ENTRYPOINT ["/fakeidp"]

24
idk/Dockerfile-test Normal file
View file

@ -0,0 +1,24 @@
ARG GO_VERSION=1.17.8
FROM golang:${GO_VERSION} as build_base
WORKDIR /
RUN ["apt-get","update","-y"]
RUN ["apt-get","install","-y","git","unixodbc","unixodbc-dev","netcat", "build-essential","musl-tools"]
RUN ["git", "clone", "https://github.com/edenhill/librdkafka.git"]
WORKDIR /librdkafka
# need to install dependencies because librdkafka does not find openssl header otherwise
RUN ["./configure", "--install-deps"]
RUN ["./configure", "--prefix", "/usr"]
RUN ["make"]
RUN ["make", "install"]
FROM build_base
WORKDIR /go/src/github.com/molecula/featurebase/
COPY . .
WORKDIR /go/src/github.com/molecula/featurebase/idk/
CMD ["go","test","-v","-mod=vendor","-tags=odbc,dynamic","./..."]

8
idk/Dockerfile-wait Normal file
View file

@ -0,0 +1,8 @@
FROM ubuntu:18.04
RUN ["apt-get", "update", "-y"]
RUN ["apt-get", "install", "-y", "postgresql-client", "curl", "netcat"]
ADD wait.sh /wait
ENTRYPOINT ["/wait"]

311
idk/Makefile Normal file
View file

@ -0,0 +1,311 @@
GO ?= go
GOOS ?= $(shell $(GO) env GOOS)
GOARCH ?= $(shell $(GO) env GOARCH)
GO_VERSION=1.17.8
GO_BUILD_FLAGS=
ODBC_ENABLED=
BINOUT ?= bin
VERSION := $(shell git describe --tags 2> /dev/null || git rev-parse --verify --short=7 HEAD)
VERSION_ID = $(VERSION)-$(GOOS)-$(GOARCH)
BUILD_TIME := $(shell date -u +%FT%T%z)
BRANCH_NAME ?= ""
# We allow setting a custom docker-compose "project". Multiple of the
# same docker-compose environment can exist simultaneously as long as
# they use different projects (the project name is prepended to
# container names and such). This is useful in a CI environment where
# we might be running multiple instances of the tests concurrently.
PROJECT ?= idk
DOCKER_COMPOSE = docker-compose -p $(PROJECT)
UNAME_P := $(shell uname -p)
ifneq ($(filter arm%,$(UNAME_P)),)
IS_ARM := 1
else
IS_ARM := 0
endif
ifneq ($(filter Darwin%,$(shell uname)),)
IS_MAC := 1
else
IS_MAC := 0
endif
LDFLAGS="-X github.com/molecula/featurebase/idk.Version=$(VERSION) -X github.com/molecula/featurebase/idk.BuildTime=$(BUILD_TIME)"
LDFLAGS_STATIC="-linkmode external -extldflags \"-static\" -X github.com/molecula/featurebase/idk.Version=$(VERSION) -X github.com/molecula/featurebase/idk.BuildTime=$(BUILD_TIME) "
export GOPRIVATE=github.com/molecula
export CGO_ENABLED=1
# Cloud Deployment environment (dev, prod, staging, etc.), specified with the value of MCLOUD_ENV
# E.g. `MCLOUD_ENV=test make docker-push-ecr`
MCLOUD_ENV ?= dev
MCLOUD_ENV_FILE=.cloud-env.$(MCLOUD_ENV)
-include $(MCLOUD_ENV_FILE)
PROFILE ?=
AWS_REGION ?=
ECR_URL ?=
ECR_REPO ?= idk
BINS := $(patsubst cmd/%,$(BINOUT)/%,$(wildcard cmd/*))
.PHONY: install build release-build release test $(BINS)
install:
ifeq ($(filter $(IS_ARM),$(IS_MAC)), 1)
export HOMEBREW_NO_AUTO_UPDATE=1 && [ "${PKG_CONFIG_PATH}" != "$$(brew --prefix openssl)/lib/pkgconfig" ] && (brew list pkg-config || brew install pkg-config) && (brew list librdkafka || brew install librdkafka) && (brew list openssl || brew install openssl) && export PKG_CONFIG_PATH="$$(brew --prefix openssl)/lib/pkgconfig"; \
$(GO) install -ldflags $(LDFLAGS) $(GO_BUILD_FLAGS) -tags dynamic ./cmd/...
else
$(GO) install -ldflags $(LDFLAGS) $(GO_BUILD_FLAGS) ./cmd/...
endif
$(BINS):
$(GO) build -ldflags $(LDFLAGS) $(GO_BUILD_FLAGS) -o $@ ./cmd/$(notdir $@)
$(BINOUT)/molecula-consumer-sql-odbc:
CGO_ENABLED=1 $(GO) build -tags=odbc -ldflags $(LDFLAGS) $(GO_BUILD_FLAGS) -o $@ ./cmd/molecula-consumer-sql
build_non_cgo:
CGO_ENABLED=0 $(GO) build -ldflags $(LDFLAGS) $(GO_BUILD_FLAGS) -o bin/ingester ./cmd/ingester
CGO_ENABLED=0 $(GO) build -ldflags $(LDFLAGS) $(GO_BUILD_FLAGS) -o bin/molecula-consumer-csv ./cmd/molecula-consumer-csv
CGO_ENABLED=0 $(GO) build -ldflags $(LDFLAGS) $(GO_BUILD_FLAGS) -o bin/molecula-consumer-kafka-static ./cmd/molecula-consumer-kafka-static
CGO_ENABLED=0 $(GO) build -ldflags $(LDFLAGS) $(GO_BUILD_FLAGS) -o bin/molecula-consumer-sql ./cmd/molecula-consumer-sql
CGO_ENABLED=0 $(GO) build -ldflags $(LDFLAGS) $(GO_BUILD_FLAGS) -o bin/molecula-consumer-github ./cmd/molecula-consumer-github
CGO_ENABLED=0 $(GO) build -ldflags $(LDFLAGS) $(GO_BUILD_FLAGS) -o bin/molecula-consumer-kinesis ./cmd/molecula-consumer-kinesis
build_cgo:
ifeq ($(GOARCH), arm64)
CGO_ENABLED=1 $(GO) build -tags dynamic $(GO_BUILD_FLAGS) -o bin/molecula-consumer-kafka ./cmd/molecula-consumer-kafka
CGO_ENABLED=1 $(GO) build -tags dynamic $(GO_BUILD_FLAGS) -o bin/molecula-consumer-kafka-delete ./cmd/molecula-consumer-kafka-delete
CGO_ENABLED=1 $(GO) build -tags dynamic $(GO_BUILD_FLAGS) -o bin/kafkagen ./cmd/kafkagen
CGO_ENABLED=1 $(GO) build -tags dynamic $(GO_BUILD_FLAGS) -o bin/kafkaput ./cmd/kafkaput
CGO_ENABLED=1 $(GO) build -tags dynamic $(GO_BUILD_FLAGS) -o bin/datagen ./cmd/datagen
CGO_ENABLED=1 $(GO) build -tags dynamic $(GO_BUILD_FLAGS) -o bin/bankgen ./cmd/bankgen
CGO_ENABLED=1 $(GO) build -tags dynamic $(GO_BUILD_FLAGS) -o bin/molecula-consumer-kafka-sasl ./cmd/molecula-consumer-kafka-sasl
endif
ifeq ($(GOARCH), amd64)
CC=/usr/bin/musl-gcc CGO_ENABLED=1 $(GO) build -tags "musl static" -ldflags $(LDFLAGS_STATIC) $(GO_BUILD_FLAGS) -o bin/molecula-consumer-kafka ./cmd/molecula-consumer-kafka
CC=/usr/bin/musl-gcc CGO_ENABLED=1 $(GO) build -tags "musl static" -ldflags $(LDFLAGS_STATIC) $(GO_BUILD_FLAGS) -o bin/molecula-consumer-kafka-delete ./cmd/molecula-consumer-kafka-delete
CC=/usr/bin/musl-gcc CGO_ENABLED=1 $(GO) build -tags "musl static" -ldflags $(LDFLAGS_STATIC) $(GO_BUILD_FLAGS) -o bin/kafkagen ./cmd/kafkagen
CC=/usr/bin/musl-gcc CGO_ENABLED=1 $(GO) build -tags "musl static" -ldflags $(LDFLAGS_STATIC) $(GO_BUILD_FLAGS) -o bin/kafkaput ./cmd/kafkaput
CC=/usr/bin/musl-gcc CGO_ENABLED=1 $(GO) build -tags "musl static" -ldflags $(LDFLAGS_STATIC) $(GO_BUILD_FLAGS) -o bin/datagen ./cmd/datagen
CC=/usr/bin/musl-gcc CGO_ENABLED=1 $(GO) build -tags "musl static" -ldflags $(LDFLAGS_STATIC) $(GO_BUILD_FLAGS) -o bin/bankgen ./cmd/bankgen
CC=/usr/bin/musl-gcc CGO_ENABLED=1 $(GO) build -tags "musl static" -ldflags $(LDFLAGS_STATIC) $(GO_BUILD_FLAGS) -o bin/molecula-consumer-kafka-sasl ./cmd/molecula-consumer-kafka-sasl
endif
CGO_ENABLED=1 $(GO) build -tags=odbc -ldflags $(LDFLAGS) $(GO_BUILD_FLAGS) -o bin/molecula-consumer-sql-odbc ./cmd/molecula-consumer-sql
BUILD_CGO ?= 0
build:
@echo GOOS=$(GOOS) GOARCH=$(GOARCH) uname -p=$(UNAME_P) build_cgo=$(BUILD_CGO)
make build_non_cgo
ifeq ($(BUILD_CGO), 1)
make build_cgo
endif
release-build:
mkdir -p build/molecula-consumer-$(VERSION_ID)/
$(MAKE) build BINOUT=build/molecula-consumer-$(VERSION_ID)/
tar -cvz -C build -f build/molecula-consumer-$(VERSION_ID).tar.gz molecula-consumer-$(VERSION_ID)/
@echo Created release build: build/molecula-consumer-$(VERSION_ID).tar.gz
release: check-clean
make release-build GOOS=linux GOARCH=amd64
make release-build GOOS=linux GOARCH=arm64
make release-build GOOS=darwin GOARCH=amd64
make release-build GOOS=darwin GOARCH=arm64
test:
$(GO) test ./...
test-local: testenv
IDK_TEST_CERT_PATH=$(shell pwd)/testenv/certs \
IDK_TEST_KAFKA_HOST=localhost:9092 \
IDK_TEST_PILOSA_GRPC_HOST=localhost:20101 \
IDK_TEST_PILOSA_HOST=localhost:10101 \
IDK_TEST_PILOSA_TLS_HOST=https://localhost:10111 \
IDK_TEST_REGISTRY_HOST=localhost:8081 \
$(GO) test $(TESTFLAGS) ./...
vendor: ../go.mod
$(GO) mod vendor
build-%:
$(DOCKER_COMPOSE) build $*
pull-%:
$(DOCKER_COMPOSE) pull $*
save-%-logs:
$(DOCKER_COMPOSE) logs $* > ./testdata/$(PROJECT)_$*_logs.txt
start-all: .pulled testenv build-wait
echo "branch name" ${BRANCH_NAME}
$(DOCKER_COMPOSE) up -d zookeeper
$(DOCKER_COMPOSE) run -T wait zookeeper 'echo "ruok" | nc -w 2 zookeeper 2181 | grep imok'
$(DOCKER_COMPOSE) up -d kafka
$(DOCKER_COMPOSE) up -d schema-registry
$(DOCKER_COMPOSE) up -d postgres
$(DOCKER_COMPOSE) run -T wait postgres pg_isready -h postgres -p 5432 -U postgres
BRANCH_NAME=${BRANCH_NAME} $(DOCKER_COMPOSE) up -d pilosa
BRANCH_NAME=${BRANCH_NAME} $(DOCKER_COMPOSE) up -d pilosa-tls
BRANCH_NAME=${BRANCH_NAME} $(DOCKER_COMPOSE) up -d pilosa-auth
$(DOCKER_COMPOSE) run -T wait pilosa curl --silent --fail http://pilosa:10101/status
$(DOCKER_COMPOSE) run -T wait pilosa-tls curl --silent --cacert /certs/ca.crt --key /certs/theclient.key --cert /certs/theclient.crt --fail https://pilosa-tls:10111/status
$(DOCKER_COMPOSE) run -T wait pilosa-auth curl --silent --fail http://pilosa-auth:10105/version
$(DOCKER_COMPOSE) run -T wait kafka nc -z kafka 9092
$(DOCKER_COMPOSE) run -T wait schema-registry curl --silent --fail http://schema-registry:8081/config
start-postgres: build-wait testenv .pulled
$(DOCKER_COMPOSE) up -d postgres
$(DOCKER_COMPOSE) run -T wait postgres pg_isready -h postgres -p 5432 -U postgres
start-pilosa: build-pilosa start-postgres build-wait testenv .pulled
$(DOCKER_COMPOSE) up -d pilosa
$(DOCKER_COMPOSE) run -T wait pilosa curl --silent --fail http://pilosa:10101/status
start-pilosa-tls: build-pilosa-tls build-wait testenv .pulled
$(DOCKER_COMPOSE) up -d pilosa-tls
$(DOCKER_COMPOSE) run -T wait pilosa-tls curl --silent --cacert /certs/ca.crt --key /certs/theclient.key --cert /certs/theclient.crt --fail https://pilosa-tls:10111/status
start-pilosa-auth: build-pilosa-auth build-wait testenv .pulled
$(DOCKER_COMPOSE) up -d pilosa-auth
$(DOCKER_COMPOSE) run -T wait pilosa-auth curl --silent --fail http://pilosa-auth:10105/version
start-zookeeper: pull-zookeeper testenv
$(DOCKER_COMPOSE) up -d zookeeper
$(DOCKER_COMPOSE) run -T wait zookeeper 'echo "ruok" | nc -w 2 zookeeper 2181 | grep imok'
start-kafka: pull-kafka start-zookeeper build-wait testenv
$(DOCKER_COMPOSE) up -d kafka
$(DOCKER_COMPOSE) run -T wait kafka nc -z kafka 9092
start-schema-registry: start-zookeeper start-kafka pull-schema-registry build-wait testenv
$(DOCKER_COMPOSE) up -d schema-registry
$(DOCKER_COMPOSE) run -T wait schema-registry curl --silent --fail http://schema-registry:8081/config
startup-old: start-postgres start-pilosa start-pilosa-tls start-pilosa-auth start-zookeeper start-kafka start-schema-registry
startup: start-all
shutdown:
$(DOCKER_COMPOSE) down -v --remove-orphans
rm .pulled
test-all: testenv
$(MAKE) startup
$(MAKE) test-run
$(DOCKER_COMPOSE) logs kafka | grep "Kafka version"
$(MAKE) shutdown
test-all-race: testenv
$(MAKE) startup
$(MAKE) test-run-race
$(MAKE) shutdown
test-all-kafka-sasl: testenv
$(MAKE) startup
$(MAKE) test-run-kafka-sasl
$(MAKE) shutdown
TCMD ?= ./...
# do "make startup", then e.g. "make test-run-local TCMD='-run=MyFavTest ./kafka'"
test-run-local:
pwd
$(DOCKER_COMPOSE) build idk-test
$(DOCKER_COMPOSE) run -T idk-test go test -mod=vendor -tags=odbc,dynamic $(TCMD)
TPKG ?= ./...
test-run: testenv vendor
$(DOCKER_COMPOSE) build idk-test
$(DOCKER_COMPOSE) run -T idk-test bash -c "set -o pipefail; go test -v -mod=vendor -tags=odbc,dynamic $(TPKG) -covermode=atomic -coverpkg=$(TPKG) -json -coverprofile=/testdata/$(PROJECT)_coverage.out | tee /testdata/$(PROJECT)_report.out"
$(DOCKER_COMPOSE) run -T idk-test /go/src/github.com/molecula/featurebase/idk/reingest_test.sh
test-run-race: testenv vendor
$(DOCKER_COMPOSE) build idk-test
$(DOCKER_COMPOSE) run -T idk-test bash -c "set -o pipefail; go test -v -mod=vendor -race -covermode=atomic -tags=dynamic $(TPKG) -coverpkg=$(TPKG) -timeout=30m -json -coverprofile=/testdata/$(PROJECT)_coverage.out | tee /testdata/$(PROJECT)_report.out"
test-run-kafka-sasl: testenv vendor
$(DOCKER_COMPOSE) build idk-test
$(DOCKER_COMPOSE) run -T idk-test bash -c "set -o pipefail; go test -v --tags=kafka_sasl -mod=vendor -race -timeout=30m $(TPKG) -covermode=atomic -coverpkg=$(TPKG) -json -coverprofile=/testdata/$(PROJECT)_coverage.out | tee /testdata/$(PROJECT)_report.out"
.pulled:
$(DOCKER_COMPOSE) pull
touch .pulled
testenv: testenv/certs
testenv/certs:
certstrap --depot-path testenv/certs init --common-name ca --passphrase "" --expires "100 years"
certstrap --depot-path=testenv/certs request-cert --domain localhost --common-name localhost --passphrase ""
certstrap --depot-path=testenv/certs sign --CA ca --expires "100 years" localhost
certstrap --depot-path=testenv/certs request-cert --domain pilosa-tls --common-name pilosa-tls --passphrase ""
certstrap --depot-path=testenv/certs sign --CA ca --expires "100 years" pilosa-tls
certstrap --depot-path=testenv/certs request-cert --domain theclient --common-name theclient --passphrase ""
certstrap --depot-path=testenv/certs sign --CA ca --expires "100 years" theclient
# Create release using Docker
docker-release:
@if [ $(IS_ARM) = 1 ]; then \
echo Building linux-amd64 image on $(UNAME_P) is not supported; \
else \
$(MAKE) docker-build GOOS=linux GOARCH=amd64; \
fi
$(MAKE) docker-build GOOS=darwin GOARCH=amd64
$(MAKE) docker-build GOOS=darwin GOARCH=arm64
$(MAKE) docker-build GOOS=linux GOARCH=arm64
# This allows multiple concurrent builds to happen in CI without
# creating container name conflicts and such. (different BUILD_NAMEs
# are passed in from gitlab-ci.yml)
BUILD_NAME ?= idk-build
# Build a release in Docker
docker-build: vendor
DOCKER_BUILDKIT=0 docker build \
-f ../idk/Dockerfile \
--build-arg GO_VERSION=$(GO_VERSION) \
--build-arg MAKE_FLAGS="GOOS=$(GOOS) GOARCH=$(GOARCH) BUILD_CGO=$(BUILD_CGO)" \
--build-arg GO_BUILD_FLAGS=$(GO_BUILD_FLAGS) \
--target builder \
--tag idk:$(BUILD_NAME) ../.
mkdir -p build/idk-$(GOOS)-$(GOARCH)
docker create --name $(BUILD_NAME) idk:$(BUILD_NAME)
docker cp $(BUILD_NAME):/featurebase/idk/bin/. ./build/idk-$(GOOS)-$(GOARCH)
docker rm $(BUILD_NAME)
# Create Docker image from Dockerfile
docker-image: vendor
docker build \
--build-arg GO_VERSION=$(GO_VERSION) \
--build-arg MAKE_FLAGS="GOOS=$(GOOS) GOARCH=$(GOARCH) BUILD_CGO=$(BUILD_CGO)" \
--tag registry.gitlab.com/molecula/featurebase/idk:$(VERSION_ID) .
@echo Created docker image: registry.gitlab.com/molecula/featurebase/idk:$(VERSION_ID)
docker: docker-image
docker-tag-push:
docker push registry.gitlab.com/molecula/featurebase/idk:$(VERSION_ID)
@echo Pushed docker image: registry.gitlab.com/molecula/featurebase/idk:$(VERSION_ID)
clean:
$(MAKE) shutdown
rm -rf testenv
# Error out if there are untracked changes in Git
check-clean:
ifndef SKIP_CHECK_CLEAN
$(if $(shell git status --porcelain),$(error Git status is not clean! Please commit or checkout/reset changes.))
endif
docker-push-ecr: docker-image aws-login
docker tag idk:$(VERSION) $(ECR_URL)/$(ECR_REPO):$(VERSION)
aws ecr get-login-password --region $(AWS_REGION) --profile $(PROFILE)| docker login --username AWS --password-stdin $(ECR_URL)
docker push $(ECR_URL)/$(ECR_REPO):$(VERSION)
aws-login:
aws sso login --profile $(PROFILE)

124
idk/README.md Normal file
View file

@ -0,0 +1,124 @@
# idk: Ingest Development Kit
## Integration tests
To run the tests, you will need to install the following dependencies:
1. [Docker](https://docs.docker.com/install/)
2. [Docker Compose](https://docs.docker.com/compose/install/)
3. [Certstrap](https://github.com/square/certstrap)
In addition to these dependancies, you will need to be added to the moleculacorp [Dockerhub](https://hub.docker.com/orgs/moleculacorp) account.
First start the test environment. This is a docker-compose environment that includes pilosa and a confluent kafka stack. Run the following to start those services:
make startup
To build and run the integration tests, run:
make test-run
Then to shut down the test environment, run:
make shutdown
You can run all of the previous commands by calling test-all:
make test-all
The previous command is equivalent to running the following:
make startup
sleep 30 # wait for services to come up
make test-run
make shutdown
To run an individual test, you can run the command directly using docker-compose. Note that you must run `docker-compose build idk-test` for docker to run the latest code. Modify the following as needed:
make startup
docker-compose build idk-test
docker-compose run idk-test /usr/local/go/bin/go test -count=1 -mod=vendor -run=TestCmdMainOne ./kafka
To shutdown and reset the environment:
make clean
## Running dependencies locally (rather than in docker) "make test-local"
This is for running the tests locally and *not* in Docker... so you
have to be running a bunch of stuff natively on your machine.
Run Pilosa with default config:
pilosa server
Run another pilosa like
pilosa server --config=pilosa-sec-test.conf
which will run Pilosa with TLS using certs in testenv. (`make testenv` first if you haven't).
You also need to be running the Confluent stack which, after you've
installed it from Confluent's site might look something like:
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_66.jdk/Contents/Home
confluent local destroy && confluent local start schema-registry
Or it might not! You may not need the first line, but if you have the
wrong Java version by default, that's how you set it. The second line
may change depending on what version of the confluent stack you get. According to
confluent version
I'm running:
Version: v0.212.0
Git Ref: 2b04985
Use the test-local make target:
make test-local
This sets a number of environment variables (which it prints when you
run it), and should set them correctly if you follow the instructions
above, but if you're running things on non-default ports you may need
to tweak them.
## CSV Ingester
1. make sure you're running Pilosa (localhost:10101 for these instructions)
molecula-consumer-csv --primary-key-fields=asset_tag -i sample-index --files sample.csv
asset_tag__String,fan_time__RecordTime_2006-01-02,fan_val__String_F_YMD
ABCD,2019-01-02,70%
ABCD,2019-01-03,20%
ABCD,2019-01-04,30%
BEDF,2019-01-02,70%
BEDF,2019-01-05,90%
BEDF,2019-01-08,10%
BEDF,2019-01-08,20%
ABCD,2019-01-30,40%
## Datagen
Datagen is an internal command-line tool to generate various application-specific datasets, and ingest them directly into Pilosa. After running `make install`, run `datagen` with no arguments to see a list of available "sources".
When running Datagen with local Docker stacks, make sure to add individual docker stacks to the `/etc/hosts` file:
127.0.0.1 kafka
127.0.0.1 pilosa
127.0.0.1 <docker stack>
## ODBC Support
By default, the SQL ingester is not built with ODBC support.
This is because it uses CGO with extra dependencies, and the resulting binaries are not portable.
In order to build with ODBC support, it is necessary to install `unixODBC` to the system.
Then run:
```
make bin/molecula-consumer-sql-odbc
```
Different Linux distros will store certain libraries in different locations.
ODBC uses dynamic library loading to handle drivers, so full static linking of dependencies is not possible.
It is therefore necessary to build on a system with the same distro and the same software versions as the target machine.

84
idk/api/codec.go Normal file
View file

@ -0,0 +1,84 @@
package api
import (
"encoding/json"
"io"
"github.com/pkg/errors"
)
// newJSONCodec creates a codec to stream a JSON array of objects, each of which represents a record.
// This returns an error if the data stream does not contain the start of a valid JSON array.
func newJSONCodec(r io.Reader) (*jsonCodec, error) {
// Create a JSON decoder.
d := json.NewDecoder(r)
// Enable the json.Number type in parsed output.
// This allows us to retain precision on numbers, rather than converting them to floating point.
d.UseNumber()
// Do not allow unknown fields.
d.DisallowUnknownFields()
// Read the opening bracket of the array.
token, err := d.Token()
if err != nil {
if err == io.EOF {
// The input included nothing (except maybe whitespace).
// This is not expected.
err = io.ErrUnexpectedEOF
}
return nil, errors.Wrap(err, "parsing start of JSON data")
}
switch token {
case json.Delim('['):
// This is the start of the array of records.
case json.Delim('{'):
// The input is a JSON object instead of an array.
// This seems like an easy user error, so provide a descriptive error.
return nil, errors.New("expected an array of records but found an object")
default:
// This is a number or something.
return nil, errors.Errorf("expected array of records but got %v", token)
}
return &jsonCodec{d: d}, nil
}
type record struct {
PrimaryKey interface{} `json:"primary-key"`
Values map[string]interface{} `json:"values"`
}
type jsonCodec struct {
d *json.Decoder
}
func (c *jsonCodec) Next() (record, error) {
if !c.d.More() {
// There are no more records in the stream.
// Read the ']'.
_, err := c.d.Token()
if err != nil {
if err == io.EOF {
// If the closing bracket is missing, then the stream was terminated prematurely.
err = io.ErrUnexpectedEOF
}
return record{}, errors.Wrap(err, "parsing end of record array")
}
// There are no more records.
return record{}, io.EOF
}
// Parse the record.
var r record
err := c.d.Decode(&r)
if err != nil {
return record{}, errors.Wrap(err, "parsing record")
}
return r, nil
}

112
idk/api/codec_test.go Normal file
View file

@ -0,0 +1,112 @@
package api
import (
"encoding/json"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestJSONCodec(t *testing.T) {
t.Parallel()
// Ensure that an empty input is handled gracefully.
t.Run("MissingInput", func(t *testing.T) {
t.Parallel()
_, err := newJSONCodec(strings.NewReader(``))
assert.EqualError(t, err, "parsing start of JSON data: unexpected EOF")
})
// Ensure that something other than an array is handled gracefully.
t.Run("Object", func(t *testing.T) {
t.Parallel()
_, err := newJSONCodec(strings.NewReader(`{}`))
assert.EqualError(t, err, "expected an array of records but found an object")
})
// Ensure that an incomplete array is handled gracefully.
t.Run("IncompleteArray", func(t *testing.T) {
t.Parallel()
codec, err := newJSONCodec(strings.NewReader(`[`))
if !assert.NoError(t, err) {
return
}
_, err = codec.Next()
assert.EqualError(t, err, "parsing end of record array: unexpected EOF")
})
// Test normal behavior.
t.Run("Normal", func(t *testing.T) {
t.Parallel()
expect := []record{
{
PrimaryKey: json.Number("2"),
Values: map[string]interface{}{
"int": json.Number("1"),
"decimal": json.Number("2.01"),
"string": "h",
"idset": []interface{}{
json.Number("1"),
json.Number("2"),
json.Number("4"),
},
"stringset": []interface{}{"plugh"},
},
},
{
PrimaryKey: "h",
Values: map[string]interface{}{},
},
}
codec, err := newJSONCodec(strings.NewReader(`[
{
"primary-key": 2,
"values": {
"int": 1,
"decimal": 2.01,
"string": "h",
"idset": [1, 2, 4],
"stringset": ["plugh"]
}
},
{
"primary-key": "h",
"values": {}
}
]`))
if !assert.NoError(t, err) {
return
}
out, err := decodeAll(codec)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, expect, out)
})
}
func decodeAll(c *jsonCodec) ([]record, error) {
var out []record
for {
rec, err := c.Next()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
out = append(out, rec)
}
return out, nil
}

183
idk/api/schema.go Normal file
View file

@ -0,0 +1,183 @@
package api
import (
"encoding/json"
"io"
"time"
pilosa "github.com/molecula/featurebase/v3"
pilosaclient "github.com/molecula/featurebase/v3/client"
"github.com/pkg/errors"
)
type schema struct {
IndexName string `json:"index-name"`
IfNotExists bool `json:"if-not-exists,omitempty"`
PrimaryKeyType string `json:"primary-key-type"`
Fields []schemaField `json:"fields"`
}
type schemaField struct {
FieldName string `json:"field-name"`
FieldType string `json:"field-type"`
FieldOptions struct {
TimeQuantum string `json:"time-quantum,omitempty"`
CacheSize int `json:"cache-size,omitempty"`
CacheType string `json:"cache-type,omitempty"`
EnforceMutualExclusion bool `json:"enforce-mutual-exclusion,omitempty"`
Scale int `json:"scale,omitempty"`
Epoch string `json:"epoch,omitempty"`
Unit string `json:"unit,omitempty"`
TTL string `json:"ttl,omitempty"`
} `json:"field-options,omitempty"`
}
// DecodeSchema takes JSON stream (io.Reader - mainly http request body)
// and tries to decode it into pilosa.Schema.
// The function returns also a map: index-name -> if-not-exists option,
// so we can swallow "conflict" error in case of creating an index with
// the name as existing one.
func DecodeSchema(r io.Reader) (*pilosaclient.Schema, map[string]bool, error) {
pilosaSchema := pilosaclient.NewSchema()
// map: index-name -> if-not-exists option
ifNotExists := make(map[string]bool)
// Create a JSON decoder.
d := json.NewDecoder(r)
// This is just a generic loop-approach where we can iterate over JSON objects
// (stream of indexes), but on daily-bases we should just decode one index.
for d.More() {
var apiSchema schema
if err := d.Decode(&apiSchema); err != nil {
return nil, nil, errors.Wrap(err, "decoding schema")
}
err := apiSchema.applyToPilosa(pilosaSchema)
if err != nil {
return nil, nil, errors.Wrapf(err, "applying schema of index %q", apiSchema.IndexName)
}
ifNotExists[apiSchema.IndexName] = apiSchema.IfNotExists
}
return pilosaSchema, ifNotExists, nil
}
// applyToPilosa validates an index schema and adds it to a Pilosa schema.
func (s schema) applyToPilosa(ps *pilosaclient.Schema) error {
if err := pilosa.ValidateName(s.IndexName); err != nil {
return errors.Wrap(err, "validating index name")
}
var keyed bool
switch s.PrimaryKeyType {
case "string":
keyed = true
case "uint":
keyed = false
case "auto":
return errors.New("auto primary keys not yet implemented")
default:
return errors.Errorf(`invalid primary-key-type %q, it must be "string", "uint" or "auto"`, s.PrimaryKeyType)
}
idx := ps.Index(s.IndexName,
pilosaclient.OptIndexTrackExistence(true),
pilosaclient.OptIndexKeys(keyed),
)
for _, f := range s.Fields {
err := f.applyToPilosa(idx)
if err != nil {
return errors.Wrapf(err, "applying field %q from schema", f.FieldName)
}
}
return nil
}
// applyToPilosa validates a field schema and adds it to an index schema.
func (f schemaField) applyToPilosa(idx *pilosaclient.Index) error {
if err := pilosa.ValidateName(f.FieldName); err != nil {
return errors.Wrap(err, "validating field name")
}
if idx.HasField(f.FieldName) {
return errors.New("field defined multiple times")
}
var opts []pilosaclient.FieldOption
switch f.FieldType {
case "id":
switch {
case f.FieldOptions.EnforceMutualExclusion:
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeMutex(pilosaclient.CacheType(f.FieldOptions.CacheType), f.FieldOptions.CacheSize)}
case f.FieldOptions.TimeQuantum != "":
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeTime(pilosaclient.TimeQuantum(f.FieldOptions.TimeQuantum))}
if f.FieldOptions.TTL != "" {
ttl, err := time.ParseDuration(f.FieldOptions.TTL)
if err != nil {
return errors.Wrapf(err, "unable to parse TTL from field %s", f.FieldName)
} else {
opts = append(opts, pilosaclient.OptFieldTTL(ttl))
}
}
default:
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeSet(pilosaclient.CacheType(f.FieldOptions.CacheType), f.FieldOptions.CacheSize)}
}
case "string":
switch {
case f.FieldOptions.EnforceMutualExclusion:
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeMutex(pilosaclient.CacheType(f.FieldOptions.CacheType), f.FieldOptions.CacheSize)}
case f.FieldOptions.TimeQuantum != "":
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeTime(pilosaclient.TimeQuantum(f.FieldOptions.TimeQuantum))}
if f.FieldOptions.TTL != "" {
ttl, err := time.ParseDuration(f.FieldOptions.TTL)
if err != nil {
return errors.Wrapf(err, "unable to parse TTL from field %s", f.FieldName)
} else {
opts = append(opts, pilosaclient.OptFieldTTL(ttl))
}
}
default:
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeSet(pilosaclient.CacheType(f.FieldOptions.CacheType), f.FieldOptions.CacheSize)}
}
case "bool":
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeBool()}
case "int":
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeInt()}
case "decimal":
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeDecimal(int64(f.FieldOptions.Scale))}
case "timestamp":
epoch := time.Unix(0, 0)
if f.FieldOptions.Epoch != "" {
var err error
epoch, err = time.Parse(time.RFC3339, f.FieldOptions.Epoch)
if err != nil {
return errors.Errorf("invalid epoch time %q for layout %q", f.FieldOptions.Epoch, time.RFC3339)
}
}
unit := f.FieldOptions.Unit
if !pilosa.IsValidTimeUnit(unit) {
return errors.Errorf("invalid time unit %q", f.FieldOptions.Unit)
}
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeTimestamp(epoch, unit)}
default:
return errors.Errorf(`invalid field-type %q, it must be "id", "string", "bool", "int", "decimal" or "timestamp"`, f.FieldType)
}
idx.Field(f.FieldName, opts...)
return nil
}

139
idk/api/schema_test.go Normal file
View file

@ -0,0 +1,139 @@
package api
import (
"strings"
"testing"
pilosaclient "github.com/molecula/featurebase/v3/client"
"github.com/stretchr/testify/assert"
)
func TestApplyToPilosa(t *testing.T) {
client, err := pilosaclient.NewClient("pilosa:10101")
if !assert.NoError(t, err) {
return
}
t.Cleanup(func() {
err := client.Close()
assert.NoError(t, err)
})
schema, err := client.Schema()
if !assert.NoError(t, err) {
return
}
if schema.HasIndex("apitestingest_unkeyed") {
err := client.DeleteIndexByName("apitestingest_unkeyed")
if !assert.NoError(t, err) {
return
}
}
schema, err = client.Schema()
if !assert.NoError(t, err) {
return
}
unkeyedIdx := schema.Index("apitestingest_unkeyed", pilosaclient.OptIndexTrackExistence(true))
err = client.SyncSchema(schema)
if !assert.NoError(t, err) {
return
}
t.Cleanup(func() {
err := client.DeleteIndex(unkeyedIdx)
assert.NoError(t, err)
})
type FieldOpt struct { //nolint:unused
TimeQuantum string `json:"time-quantum,omitempty"`
CacheSize int `json:"cache-size,omitempty"`
CacheType string `json:"cache-type,omitempty"`
EnforceMutualExclusion bool `json:"enforce-mutual-exclusion,omitempty"`
Scale int `json:"scale,omitempty"`
Epoch string `json:"epoch,omitempty"`
Unit string `json:"unit,omitempty"`
TTL string `json:"ttl,omitempty"`
}
tests := []struct {
name string
schemaField schemaField
expErr string
expOption string
}{
{
name: "id_ttl",
schemaField: schemaField{
FieldName: "id_ttl",
FieldType: "id",
FieldOptions: FieldOpt{"YMD", 0, "", false, 0, "", "", "11s"},
},
expErr: "",
expOption: `"{"options":{"noStandardView":false,"timeQuantum":"YMD","ttl":"11s","type":"time"}}"}`,
},
{
name: "id_bad_ttl",
schemaField: schemaField{
FieldName: "id_bad_ttl",
FieldType: "id",
FieldOptions: FieldOpt{"YMD", 0, "", false, 0, "", "", "bad-ttl"},
},
expErr: "unable to parse TTL",
expOption: `"{"options":{"type":"set"}}"}`,
},
{
name: "id_ttl_without_time_quantum",
schemaField: schemaField{
FieldName: "id_ttl_without_time_quantum",
FieldType: "id",
FieldOptions: FieldOpt{"", 0, "", false, 0, "", "", "12s"},
},
expErr: "",
expOption: `options: "{"options":{"type":"set"}}"}`,
},
{
name: "string_ttl",
schemaField: schemaField{
FieldName: "string_ttl",
FieldType: "string",
FieldOptions: FieldOpt{"YMD", 0, "", false, 0, "", "", "13s"},
},
expErr: "",
expOption: `"{"options":{"noStandardView":false,"timeQuantum":"YMD","ttl":"13s","type":"time"}}"}`,
},
{
name: "string_bad_ttl",
schemaField: schemaField{
FieldName: "string_bad_ttl",
FieldType: "string",
FieldOptions: FieldOpt{"YMD", 0, "", false, 0, "", "", "bad-ttl"},
},
expErr: "unable to parse TTL",
expOption: `"{"options":{"type":"set"}}"}`,
},
{
name: "string_ttl_without_time_quantum",
schemaField: schemaField{
FieldName: "string_ttl_without_time_quantum",
FieldType: "string",
FieldOptions: FieldOpt{"", 0, "", false, 0, "", "", "14s"},
},
expErr: "",
expOption: `options: "{"options":{"type":"set"}}"}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := (test.schemaField).applyToPilosa(unkeyedIdx)
if err != nil && !strings.Contains(err.Error(), test.expErr) {
t.Errorf("expected error: '%s', got: '%s'", test.expErr, err.Error())
}
if !strings.Contains(unkeyedIdx.String(), test.expOption) {
t.Errorf("expected option: '%v', got: '%v'", test.expOption, unkeyedIdx.String())
}
})
}
}

477
idk/api/source.go Normal file
View file

@ -0,0 +1,477 @@
package api
import (
"context"
"encoding/json"
"fmt"
"io"
"reflect"
"strconv"
"github.com/molecula/featurebase/v3/idk"
"github.com/pkg/errors"
pilosaclient "github.com/molecula/featurebase/v3/client"
"github.com/molecula/featurebase/v3/pql"
)
func IngestJSON(index *pilosaclient.Index, m idk.Main, in io.Reader) error {
codec, err := newJSONCodec(in)
if err != nil {
return errors.Wrap(err, "reading start of JSON data")
}
fields := index.Fields()
idkSchema := make([]idk.Field, 1, len(index.Fields())+1)
fieldMappers := make(map[string]mapper, len(fields))
if index.Opts().Keys() {
idkSchema[0] = idk.StringField{
NameVal: "/",
}
m.PrimaryKeyFields = []string{"/"}
} else {
idkSchema[0] = idk.IDField{
NameVal: "/",
}
m.IDField = "/"
}
m.Index = index.Name()
// TODO: time quantums
for name, field := range fields {
i := len(idkSchema)
fopts := field.Opts()
switch typ := fopts.Type(); typ {
case pilosaclient.FieldTypeSet:
if fopts.Keys() {
idkSchema = append(idkSchema, idk.StringArrayField{
NameVal: name,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
var set []string
switch v := v.(type) {
case string:
set = []string{v}
case []interface{}:
set = make([]string, len(v))
for i, elem := range v {
str, ok := elem.(string)
if !ok {
return nil, errors.Wrapf(TypeError{
Expected: typeDescriptionString,
Value: elem,
}, "parsing element %d of set", i)
}
set[i] = str
}
if len(set) > 1 {
dups := make(map[string]struct{}, len(set))
for _, elem := range set {
if _, dup := dups[elem]; dup {
return nil, ErrDuplicateElement{
Elem: elem,
}
}
dups[elem] = struct{}{}
}
}
default:
return nil, TypeError{
Expected: typeDescriptionStringSet,
Value: v,
}
}
for _, v := range set {
if v == "" {
return nil, errors.New("empty string in set")
}
}
return set, nil
},
}
} else {
idkSchema = append(idkSchema, idk.IDArrayField{
NameVal: name,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
var set []uint64
switch v := v.(type) {
case json.Number:
id, err := strconv.ParseUint(string(v), 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "parsing ID")
}
set = []uint64{id}
case []interface{}:
set = make([]uint64, len(v))
for i, elem := range v {
num, ok := elem.(json.Number)
if !ok {
return nil, errors.Wrapf(TypeError{
Expected: typeDescriptionID,
Value: elem,
}, "parsing element %d of set", i)
}
id, err := strconv.ParseUint(string(num), 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "parsing element %d of set", i)
}
set[i] = id
}
if len(set) > 1 {
dups := make(map[uint64]struct{}, len(set))
for _, elem := range set {
if _, dup := dups[elem]; dup {
return nil, ErrDuplicateElement{
Elem: elem,
}
}
dups[elem] = struct{}{}
}
}
default:
return nil, TypeError{
Expected: typeDescriptionIDSet,
Value: v,
}
}
for _, v := range set {
if v == ^uint64(0) {
// The client package uses this as a nil sentinel.
return nil, errors.New("max uint64 is not a valid row ID")
}
}
return set, nil
},
}
}
case pilosaclient.FieldTypeMutex:
if fopts.Keys() {
idkSchema = append(idkSchema, idk.StringField{
NameVal: name,
Mutex: true,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
str, ok := v.(string)
if !ok {
return nil, TypeError{
Expected: typeDescriptionString,
Value: v,
}
}
return str, nil
},
}
} else {
idkSchema = append(idkSchema, idk.IDField{
NameVal: name,
Mutex: true,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
num, ok := v.(json.Number)
if !ok {
return nil, TypeError{
Expected: typeDescriptionID,
Value: v,
}
}
id, err := strconv.ParseUint(string(num), 10, 64)
if err != nil {
return nil, err
}
if id == ^uint64(0) {
// The client package uses this as a nil sentinel.
return nil, errors.New("max uint64 is not a valid row ID")
}
return id, nil
},
}
}
/*
case pilosaclient.FieldTypeBool:
// TODO: not supported by IDK :)
idkSchema = append(idkSchema, idk.BoolField{
NameVal: name,
})
*/
case pilosaclient.FieldTypeInt:
if fopts.Keys() {
idkSchema = append(idkSchema, idk.StringField{
NameVal: name,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
str, ok := v.(string)
if !ok {
return nil, TypeError{
Expected: typeDescriptionString,
Value: v,
}
}
return str, nil
},
}
break
}
idkSchema = append(idkSchema, idk.IntField{
NameVal: name,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
number, ok := v.(json.Number)
if !ok {
return nil, TypeError{
Expected: typeDescriptionInt,
Value: v,
}
}
return strconv.ParseInt(string(number), 10, 64)
},
}
case pilosaclient.FieldTypeDecimal:
scale := fopts.Scale()
idkSchema = append(idkSchema, idk.DecimalField{
NameVal: name,
Scale: scale,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
var raw string
switch v := v.(type) {
case json.Number:
raw = string(v)
case string:
raw = string(v)
default:
return nil, TypeError{
Expected: typeDescriptionDecimal,
Value: v,
}
}
dec, err := pql.ParseDecimal(raw)
if err != nil {
return nil, err
}
return dec, nil
},
}
/*
// Pilosa unfortunately does not return the epoch to us.
// As a result, this does not currently work.
// Also now we need to parse the timestamp here.
case pilosaclient.FieldTypeTimestamp:
idkSchema = append(idkSchema, idk.TimestampField{
NameVal: name,
Epoch: fopts.MinTimestamp(),
Granularity: fopts.TimeUnit(),
Layout: time.RFC3339Nano,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
str, ok := v.(string)
if !ok {
return nil, TypeError{
Expected: "time string",
Value: v,
}
}
return str, nil
},
}*/
default:
// Ignore the field for now.
}
}
src := source{
c: codec,
fields: fieldMappers,
schema: idkSchema,
pkstring: index.Opts().Keys(),
}
m.NewSource = func() (idk.Source, error) {
return &src, nil
}
return m.Run()
}
// TypeError is an error indicating that the type of a value was set incorrectly.
type TypeError struct {
// Expected is a human-readable name of the expected type.
Expected string
// Value is the incorrectly-typed value.
Value interface{}
}
const (
typeDescriptionID = "ID"
typeDescriptionIDSet = "set of " + typeDescriptionID + "s"
typeDescriptionString = "string"
typeDescriptionStringSet = "set of " + typeDescriptionString + "s"
typeDescriptionInt = "integer"
typeDescriptionDecimal = "decimal"
)
func (t TypeError) Error() string {
valType := reflect.TypeOf(t.Value)
valTypeName := valType.String()
if name, ok := friendlyTypeNames[valType]; ok {
valTypeName = name
}
return fmt.Sprintf("expected a %s but got %v (a %s)", t.Expected, t.Value, valTypeName)
}
// friendlyTypeNames contains more-easily understood names for some common types.
var friendlyTypeNames = map[reflect.Type]string{
reflect.TypeOf(json.Number("")): "number",
reflect.TypeOf([]interface{}{}): "array",
reflect.TypeOf(map[string]interface{}{}): "object",
}
// ErrDuplicateElement is an error indicating that an element was included in a set multiple times.
type ErrDuplicateElement struct {
// Elem is the duplicated element.
Elem interface{}
}
func (err ErrDuplicateElement) Error() string {
return fmt.Sprintf("found duplicate element %v in set", err.Elem)
}
type source struct {
c *jsonCodec
pkstring bool
fields map[string]mapper
schema []idk.Field
}
type mapper struct {
idx int
mapper func(interface{}) (interface{}, error)
}
func (s *source) Record() (idk.Record, error) {
// Decode the next record.
raw, err := s.c.Next()
if err != nil {
return nil, err
}
// Parse the record into IDK-format.
out := make(idkRec, len(s.schema))
switch pk := raw.PrimaryKey.(type) {
case string:
if !s.pkstring {
return nil, errors.Wrap(TypeError{
Expected: typeDescriptionID,
Value: pk,
}, "parsing primary key")
}
out[0] = pk
case json.Number:
if s.pkstring {
return nil, errors.Wrap(TypeError{
Expected: typeDescriptionString,
Value: pk,
}, "parsing primary key")
}
id, err := strconv.ParseUint(string(pk), 10, 64)
if err != nil {
return nil, errors.Wrap(err, "parsing primary key")
}
out[0] = id
default:
expect := typeDescriptionID
if s.pkstring {
expect = typeDescriptionString
}
return nil, errors.Wrap(TypeError{
Expected: expect,
Value: pk,
}, "parsing primary key")
}
for field, value := range raw.Values {
m, ok := s.fields[field]
if !ok {
return nil, errors.Errorf("field %q missing from schema", field)
}
v, err := m.mapper(value)
if err != nil {
return nil, errors.Wrapf(err, "parsing record field %q", field)
}
out[m.idx] = v
}
return out, nil
}
func (s *source) Schema() []idk.Field {
return s.schema
}
func (s *source) Close() error {
return nil
}
type idkRec []interface{}
func (r idkRec) Commit(ctx context.Context) error {
return nil
}
func (r idkRec) Data() []interface{} {
return r
}

225
idk/api/source_test.go Normal file
View file

@ -0,0 +1,225 @@
package api
import (
"io"
"io/ioutil"
"net/http"
"strings"
"testing"
pilosaclient "github.com/molecula/featurebase/v3/client"
"github.com/molecula/featurebase/v3/idk"
"github.com/stretchr/testify/assert"
)
func TestIngest(t *testing.T) {
if testing.Short() {
t.Skip()
}
client, err := pilosaclient.NewClient("pilosa:10101")
if !assert.NoError(t, err) {
return
}
t.Cleanup(func() {
err := client.Close()
assert.NoError(t, err)
})
schema, err := client.Schema()
if !assert.NoError(t, err) {
return
}
if schema.HasIndex("apitestingest_unkeyed") {
err := client.DeleteIndexByName("apitestingest_unkeyed")
if !assert.NoError(t, err) {
return
}
}
schema, err = client.Schema()
if !assert.NoError(t, err) {
return
}
unkeyedIdx := schema.Index("apitestingest_unkeyed", pilosaclient.OptIndexTrackExistence(true))
unkeyedIdx.Field("idset")
unkeyedIdx.Field("id", pilosaclient.OptFieldTypeMutex(pilosaclient.CacheTypeNone, 0))
unkeyedIdx.Field("stringset", pilosaclient.OptFieldKeys(true))
unkeyedIdx.Field("string", pilosaclient.OptFieldKeys(true), pilosaclient.OptFieldTypeMutex(pilosaclient.CacheTypeNone, 0))
//unkeyedIdx.Field("bool", pilosaclient.OptFieldTypeBool())
unkeyedIdx.Field("int", pilosaclient.OptFieldTypeInt())
unkeyedIdx.Field("decimal", pilosaclient.OptFieldTypeDecimal(2))
//unkeyedIdx.Field("timestamp", pilosaclient.OptFieldTypeTimestamp(time.Unix(1, 0).UTC(), "s"))
keyedIdx := schema.Index("apitestingest_keyed", pilosaclient.OptIndexTrackExistence(true), pilosaclient.OptIndexKeys(true))
keyedIdx.Field("idset")
keyedIdx.Field("id", pilosaclient.OptFieldTypeMutex(pilosaclient.CacheTypeNone, 0))
keyedIdx.Field("stringset", pilosaclient.OptFieldKeys(true))
keyedIdx.Field("string", pilosaclient.OptFieldKeys(true), pilosaclient.OptFieldTypeMutex(pilosaclient.CacheTypeNone, 0))
//keyedIdx.Field("bool", pilosaclient.OptFieldTypeBool())
keyedIdx.Field("int", pilosaclient.OptFieldTypeInt())
keyedIdx.Field("decimal", pilosaclient.OptFieldTypeDecimal(2))
//keyedIdx.Field("timestamp", pilosaclient.OptFieldTypeTimestamp(time.Unix(1, 0).UTC(), "s"))
err = client.SyncSchema(schema)
if !assert.NoError(t, err) {
return
}
t.Cleanup(func() {
err := client.DeleteIndex(unkeyedIdx)
assert.NoError(t, err)
err = client.DeleteIndex(keyedIdx)
assert.NoError(t, err)
})
m := *idk.NewMain()
m.PilosaHosts = []string{"pilosa:10101"}
m.PilosaGRPCHosts = []string{"pilosa:20101"}
m.Verbose = true
m.Pprof = ""
m.Stats = ""
m.PackBools = ""
t.Run("Unkeyed", func(t *testing.T) {
t.Parallel()
err := IngestJSON(unkeyedIdx, m, strings.NewReader(`[
{
"primary-key": 0,
"values": {
"idset": [5, 7],
"id": 4,
"stringset": ["a"],
"string": "h",
"int": -13,
"decimal": 1.01
}
},
{
"primary-key": 1,
"values": {
"idset": 6,
"id": 8,
"stringset": "b",
"decimal": "1.02"
}
},
{
"primary-key": 2,
"values": {}
}
]`))
if !assert.NoError(t, err) {
return
}
// Unfortunately the go-pilosa client is still very behind.
resp, err := http.Post("http://pilosa:10101/index/apitestingest_unkeyed/query", "text/plain", strings.NewReader("Extract(All(), Rows(idset), Rows(id), Rows(stringset), Rows(string), Rows(int), Rows(decimal))"))
if !assert.NoError(t, err) {
return
}
defer resp.Body.Close()
defer io.Copy(ioutil.Discard, resp.Body) //nolint: errcheck
if !assert.Equal(t, 200, resp.StatusCode) {
body, _ := ioutil.ReadAll(resp.Body)
t.Logf("request error: %s", body)
return
}
data, err := ioutil.ReadAll(resp.Body)
if !assert.NoError(t, err) {
return
}
expect := `{
"results": [
{
"fields": [
{"name":"idset","type":"[]uint64"},
{"name":"id","type":"uint64"},
{"name":"stringset","type":"[]string"},
{"name":"string","type":"string"},
{"name":"int","type":"int64"},
{"name":"decimal","type":"decimal"}
],
"columns": [
{"column":0,"rows":[[5,7],4,["a"],"h",-13,1.01]},
{"column":1,"rows":[[6],8,["b"],null,null,1.02]},
{"column":2,"rows":[[],null,[],null,null,null]}
]
}
]
}`
assert.JSONEq(t, expect, string(data))
})
t.Run("Keyed", func(t *testing.T) {
t.Parallel()
err := IngestJSON(keyedIdx, m, strings.NewReader(`[
{
"primary-key": "a",
"values": {
"idset": [5, 7],
"id": 4,
"stringset": ["a"],
"string": "h",
"int": -13,
"decimal": 1.01
}
},
{
"primary-key": "b",
"values": {
"idset": 6,
"id": 8,
"stringset": "b",
"decimal": "1.02"
}
},
{
"primary-key": "c",
"values": {}
}
]`))
if !assert.NoError(t, err) {
return
}
// Unfortunately the go-pilosa client is still very behind.
resp, err := http.Post("http://pilosa:10101/index/apitestingest_keyed/query", "text/plain", strings.NewReader("Extract(All(), Rows(idset), Rows(id), Rows(stringset), Rows(string), Rows(int), Rows(decimal))"))
if !assert.NoError(t, err) {
return
}
defer resp.Body.Close()
defer io.Copy(ioutil.Discard, resp.Body) //nolint: errcheck
if !assert.Equal(t, 200, resp.StatusCode) {
body, _ := ioutil.ReadAll(resp.Body)
t.Logf("request error: %s", body)
return
}
data, err := ioutil.ReadAll(resp.Body)
if !assert.NoError(t, err) {
return
}
expect := `{
"results": [
{
"fields": [
{"name":"idset","type":"[]uint64"},
{"name":"id","type":"uint64"},
{"name":"stringset","type":"[]string"},
{"name":"string","type":"string"},
{"name":"int","type":"int64"},
{"name":"decimal","type":"decimal"}
],
"columns": [
{"column":"a","rows":[[5,7],4,["a"],"h",-13,1.01]},
{"column":"b","rows":[[6],8,["b"],null,null,1.02]},
{"column":"c","rows":[[],null,[],null,null,null]}
]
}
]
}`
assert.JSONEq(t, expect, string(data))
})
}

315
idk/bankgen/put.go Normal file
View file

@ -0,0 +1,315 @@
package bankgen
import (
"encoding/binary"
"fmt"
"io"
"math/rand"
"os"
"strconv"
"strings"
"sync/atomic"
liavro "github.com/linkedin/goavro/v2"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/common"
"github.com/molecula/featurebase/v3/idk/kafka/csrc"
"github.com/molecula/featurebase/v3/logger"
"github.com/pkg/errors"
confluent "github.com/confluentinc/confluent-kafka-go/kafka"
)
// PutCmd represents a command to put data into Kafka
type PutCmd struct {
idk.ConfluentCommand `flag:"!embed"`
Topic string `help:"Kafka topic to post to."`
Subject string
Seed int64 `help:"Random seed for data generation"`
Save string `help:"Filename of record log. '-' for stdout."`
BatchSize int `help:"Size of record batches to submit to Kafka."`
NumRecords int `help:"Number of records to generate"`
DryRun bool `help:"Dry run - just flag parsing."`
log logger.Logger
rand *rand.Rand
configMap *confluent.ConfigMap
}
// NewPutCmd creates a new PutCmd
func NewPutCmd() (*PutCmd, error) {
var err error
p := &PutCmd{}
p.ConfluentCommand = idk.ConfluentCommand{
KafkaBootstrapServers: []string{"localhost:9092"},
SchemaRegistryURL: "localhost:8081",
}
p.Topic = "banktest"
p.Subject = "test"
p.Save = ""
p.BatchSize = 1000
p.NumRecords = 10
p.log = logger.NewVerboseLogger(os.Stderr)
return p, err
}
func (p *PutCmd) Log() logger.Logger { return p.log }
// Run is the entry point for PutCmd
func (p *PutCmd) Run() (err error) {
p.configMap, err = common.SetupConfluent(&p.ConfluentCommand)
if err != nil {
return
}
var auth *csrc.BasicAuth
if len(p.SchemaRegistryUsername) > 0 {
auth = &csrc.BasicAuth{
KafkaSchemaApiKey: p.SchemaRegistryUsername,
KafkaSchemaApiSecret: p.SchemaRegistryPassword,
}
}
client := csrc.NewClient(p.SchemaRegistryURL, nil, auth)
schemaStr := getBankSchema()
resp, err := client.PostSubjects(p.Subject, schemaStr)
if err != nil {
return errors.Wrap(err, "posting schema")
}
p.log.Printf("Posted schema ID: %d\n", resp.ID)
licodec, err := liavro.NewCodec(schemaStr)
if err != nil {
return errors.Wrap(err, "li decoding schema")
}
p.rand = rand.New(rand.NewSource(p.Seed))
var w io.Writer = os.Stdout
if p.Save != "" && p.Save != "-" {
f, err := os.Create(p.Save)
if err != nil {
return errors.Wrap(err, "creating record log file")
}
defer f.Close()
w = f
}
// librdkafka has a large (1000000) default buffer.
// limit the go buffer size to reduce memory consumption
// but allows for the intended buffered channel behavior
err = p.configMap.SetKey("go.produce.channel.size", 1)
if err != nil {
return errors.Wrap(err, "setting go.produce.channel.size")
}
producer, err := confluent.NewProducer(p.configMap)
if err != nil {
fmt.Printf("Failed to create producer: %s\n", err)
os.Exit(1)
}
defer producer.Close()
finished := int32(0)
iter := int64(0)
doneChan := common.LaunchKafkaEventConfirmer(producer, &finished, &iter)
for n := 0; n < p.NumRecords; n++ {
data := p.generateRecord()
rec, _, err := licodec.NativeFromTextual([]byte(data))
if err != nil {
return errors.Wrap(err, "decoding generated data")
}
buf := make([]byte, 5, 1000)
buf[0] = 0
binary.BigEndian.PutUint32(buf[1:], uint32(resp.ID))
buf, err = licodec.BinaryFromNative(buf, rec.(map[string]interface{}))
if err != nil {
return errors.Errorf("encoding:\n%+v\nerr: %v", rec, err)
}
var uid int
switch v := rec.(map[string]interface{})["user_id"].(type) {
case int:
uid = v
case int32:
uid = int(v)
}
producer.ProduceChannel() <- &confluent.Message{
TopicPartition: confluent.TopicPartition{Topic: &p.Topic, Partition: confluent.PartitionAny},
Key: []byte(strconv.Itoa(uid)),
Value: buf,
}
atomic.AddInt64(&iter, 1)
if p.Save != "" {
fmt.Fprintf(w, "%v\n", rec.(map[string]interface{}))
}
}
atomic.AddInt32(&finished, 1)
<-doneChan //wait till all messages are acked
producer.Flush(10 * 1000)
p.log.Printf("Put %d generated records\n", iter)
return nil
}
var (
floatMeta = `"type": ["null", {"type": "float", "scale": 3}], "default": null`
boolMeta = `"type": ["null", "boolean"], "default": null`
stringMeta = `"type": ["null", "string"], "default": null, "mutex": true`
intMeta = `"type": ["null", "int"], "default": null`
floatNames = []string{
"pfm_category_total_current_balance__personal_loan",
}
boolNames = []string{
"pfm_boolean__personal_loan",
"pfm_boolean__mortgage",
}
stringNames = []string{
"survey_5dfe1a89_29fa_4505_ac00_4cd055b758ab",
}
intNames = []string{
"product_recency__consumer_loan",
}
floatCount = 1
boolCount = 2
stringCount = 1
intCount = 1
floatNullChance = 0.1
boolNullChance = 0.1
stringNullChance = 0.1
intNullChance = 0.1
)
func getBankSchema() string {
meta := `"type": "record",
"name": "bank_user_data",
"namespace": "bank.user.data",
"doc": "Per-user bank data"`
fields := []string{
`{"name": "aba", "type": "string"}`,
`{"name": "db", "type": "string"}`,
`{"name": "user_id", "type": "int"}`,
`{"name": "custom_audiences", "type": ["null", "string"], "default": null}`,
}
for n := 0; n < floatCount; n++ {
fields = append(fields, `{"name": "float_`+floatNames[n]+`", `+floatMeta+`}`)
}
for n := 0; n < boolCount; n++ {
fields = append(fields, `{"name": "bool_`+boolNames[n]+`", `+boolMeta+`}`)
}
for n := 0; n < stringCount; n++ {
fields = append(fields, `{"name": "string_`+stringNames[n]+`", `+stringMeta+`}`)
}
for n := 0; n < intCount; n++ {
fields = append(fields, `{"name": "int_`+intNames[n]+`", `+intMeta+`}`)
}
schema := fmt.Sprintf(`{%s, "fields": [%s]}`, meta, strings.Join(fields, ", "))
return schema
}
func (p *PutCmd) generateRecord() string {
fields := []string{
`"aba": "` + p.ABA() + `"`,
`"db": "` + p.Db() + `"`,
`"user_id": ` + strconv.FormatInt(int64(p.UserID()), 10),
`"custom_audiences": {"string": "` + p.CustomAudiences() + `"}`,
}
for n := 0; n < floatCount; n++ {
if floatNullChance < p.rand.Float64() {
floatStr := strconv.FormatFloat(p.rand.Float64(), 'f', 3, 64)
fields = append(fields, `"float_`+floatNames[n]+`": {"float": `+floatStr+`}`)
}
}
for n := 0; n < boolCount; n++ {
if boolNullChance < p.rand.Float64() {
boolStr := strconv.FormatBool(p.rand.Intn(2) == 1)
fields = append(fields, `"bool_`+boolNames[n]+`": {"boolean": `+boolStr+`}`)
}
}
for n := 0; n < stringCount; n++ {
if stringNullChance < p.rand.Float64() {
stringStr := text(p.rand, 1, 6, true, true, true, false)
fields = append(fields, `"string_`+stringNames[n]+`": {"string": "`+stringStr+`"}`)
}
}
for n := 0; n < intCount; n++ {
if intNullChance < p.rand.Float64() {
intStr := strconv.FormatInt(int64(p.rand.Intn(1000000)), 10)
fields = append(fields, `"int_`+intNames[n]+`": {"int": `+intStr+`}`)
}
}
return `{` + strings.Join(fields, ", ") + `}`
}
// ABA returns a random 9 numeric digit string with about 27000 possible values.
func (p *PutCmd) ABA() string {
num := p.rand.Intn(27000) + 22213
num2 := num/10 - 1213
numstr := strconv.Itoa(num)
num2str := strconv.Itoa(num2)
numstrbytes := append([]byte(numstr), num2str[3], numstr[0], numstr[1], numstr[2])
return string(numstrbytes)
}
// Db returns a db
func (p *PutCmd) Db() string {
return text(p.rand, 1, 6, true, true, true, false)
}
// UserID returns a user ID
func (p *PutCmd) UserID() int {
return p.rand.Intn(10000000) // 10 mil
}
// CustomAudiences returns a fake Custom Audience string
func (p *PutCmd) CustomAudiences() string {
return text(p.rand, 1, 6, true, true, true, false)
}
var lowerLetters = []rune("abcdefghijklmnopqrstuvwxyz")
var upperLetters = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
var numeric = []rune("0123456789")
var specialChars = []rune(`!'@#$%^&*()_+-=[]{};:",./?`)
func text(rand *rand.Rand, atLeast, atMost int, allowLower, allowUpper, allowNumeric, allowSpecial bool) string {
allowedChars := []rune{}
if allowLower {
allowedChars = append(allowedChars, lowerLetters...)
}
if allowUpper {
allowedChars = append(allowedChars, upperLetters...)
}
if allowNumeric {
allowedChars = append(allowedChars, numeric...)
}
if allowSpecial {
allowedChars = append(allowedChars, specialChars...)
}
result := []rune{}
nTimes := rand.Intn(atMost-atLeast+1) + atLeast
for i := 0; i < nTimes; i++ {
result = append(result, allowedChars[rand.Intn(len(allowedChars))])
}
return string(result)
}

26
idk/bankgen/put_test.go Normal file
View file

@ -0,0 +1,26 @@
package bankgen
import (
"fmt"
"math/rand"
"testing"
"time"
)
func TestBankgen(t *testing.T) {
p, err := NewPutCmd()
if err != nil {
t.Fatalf("failed to get NewPutCmd: %v", err)
}
p.KafkaBootstrapServers = []string{"kafka:9092"}
p.SchemaRegistryURL = "http://schema-registry:8081"
p.Topic = fmt.Sprintf("topic-%d", rand.New(rand.NewSource(time.Now().Unix())).Uint64())
p.NumRecords = 100
err = p.Run()
if err != nil {
t.Fatalf("failed to run bankgen: %v", err)
}
}

35
idk/cmd/bankgen/main.go Normal file
View file

@ -0,0 +1,35 @@
package main
import (
"log"
"os"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/bankgen"
"github.com/molecula/featurebase/v3/logger"
)
func main() {
m, err := bankgen.NewPutCmd()
if err != nil {
log.Fatal(err)
}
if err := pflag.LoadEnv(m, "BANKGEN_", nil); err != nil {
log.Fatal(err)
}
if m.DryRun {
log.Printf("%+v\n", m)
return
}
if err := m.Run(); err != nil {
log := m.Log()
if log == nil {
// if we fail before a logger was instantiated
logger.NewStandardLogger(os.Stderr).Errorf("Error running command: %v", err)
os.Exit(1)
}
log.Errorf("Error running command: %v", err)
os.Exit(1)
}
}

View file

@ -0,0 +1,129 @@
package main
import (
"reflect"
"testing"
"github.com/jaffee/commandeer"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/bankgen"
pflag13 "github.com/spf13/pflag"
)
func TestBankgenArgs(t *testing.T) {
tests := []struct {
name string
args []string
KafkaHosts []string
RegistryURL string
Topic string
Subject string
Save string
NumRecords int
Seed int64
BatchSize int
}{
{
name: "empty",
args: []string{
"", // os.Args[0] can be ignored
},
KafkaHosts: []string{"localhost:9092"},
RegistryURL: "localhost:8081",
Topic: "banktest",
Subject: "test",
Save: "",
NumRecords: 10,
},
{
name: "many-kafka-hosts",
args: []string{
"bankgen", // os.Args[0] can be ignored
"--kafka-bootstrap-servers", "localhost:0,localhost:80,localhost:9092",
"--num-records", "123",
"--schema-registry-url", "https://localhost:8081",
"--save", "-",
"--subject", "test",
"--topic", "test",
"--batch-size", "30",
},
KafkaHosts: []string{"localhost:0", "localhost:80", "localhost:9092"},
RegistryURL: "https://localhost:8081",
Topic: "test",
Subject: "test",
Save: "-",
NumRecords: 123,
BatchSize: 30,
},
{
name: "with-seed",
args: []string{
"bankgen", // os.Args[0] can be ignored
"--kafka-bootstrap-servers", "kafka:9092",
"--num-records", "10",
"--schema-registry-url", "localhost:8081",
"--save", "/tmp/file.log",
"--seed", "1234567890",
"--subject", "subject",
"--topic", "banktest",
},
KafkaHosts: []string{"kafka:9092"},
RegistryURL: "localhost:8081",
Topic: "banktest",
Subject: "subject",
Save: "/tmp/file.log",
NumRecords: 10,
Seed: int64(1234567890),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fs := &pflag.FlagSet{FlagSet: pflag13.NewFlagSet(tc.args[0], pflag13.ExitOnError)}
m, err := bankgen.NewPutCmd()
if err != nil {
t.Fatal(err)
}
if err := commandeer.LoadArgsEnv(fs, m, tc.args[1:], "BANKGEN_", nil); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(tc.KafkaHosts, m.KafkaBootstrapServers) {
t.Fatalf("--kafka-bootstrap-servers expected: %v got: %v", tc.KafkaHosts, m.KafkaBootstrapServers)
}
if !reflect.DeepEqual(tc.RegistryURL, m.SchemaRegistryURL) {
t.Fatalf("--schema-registry-url expected: %v got: %v", tc.RegistryURL, m.SchemaRegistryURL)
}
if !reflect.DeepEqual(tc.Topic, m.Topic) {
t.Fatalf("--topic expected: %v got: %v", tc.Topic, m.Topic)
}
if !reflect.DeepEqual(tc.Subject, m.Subject) {
t.Fatalf("--subject expected: %v got: %v", tc.Subject, m.Subject)
}
if !reflect.DeepEqual(tc.Save, m.Save) {
t.Fatalf("--save expected: %v got: %v", tc.Save, m.Save)
}
if !reflect.DeepEqual(tc.NumRecords, m.NumRecords) {
t.Fatalf("--num-records expected: %v got: %v", tc.NumRecords, m.NumRecords)
}
if tc.BatchSize != 0 {
if !reflect.DeepEqual(tc.BatchSize, m.BatchSize) {
t.Fatalf("--batch-size expected: %v got: %v", tc.BatchSize, m.BatchSize)
}
}
if tc.Seed != 0 {
if !reflect.DeepEqual(tc.Seed, m.Seed) {
t.Fatalf("--seed expected: %v got: %v", tc.Seed, m.Seed)
}
}
})
}
}

52
idk/cmd/datagen/main.go Normal file
View file

@ -0,0 +1,52 @@
package main
import (
"log"
"os"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/datagen"
"github.com/molecula/featurebase/v3/logger"
"gopkg.in/DataDog/dd-trace-go.v1/profiler"
)
func main() {
m := datagen.NewMain()
if err := pflag.LoadEnv(m, "GEN_", nil); err != nil {
log.Fatal(err)
}
if m.Datadog {
err := profiler.Start(
profiler.WithService("datagen"),
profiler.WithEnv("fb-1253"),
profiler.WithVersion("v1"),
profiler.WithProfileTypes(
profiler.CPUProfile,
profiler.HeapProfile,
profiler.BlockProfile,
profiler.MutexProfile,
profiler.GoroutineProfile,
),
)
if err != nil {
log.Fatal(err)
}
defer profiler.Stop()
}
if err := m.Preload(); err != nil {
log.Fatal(err)
}
m.PrintPlan()
if m.DryRun {
log.Printf("%+v\n", m)
return
}
if err := m.Run(); err != nil {
logger.NewStandardLogger(os.Stderr).Errorf("Error running command: %v", err)
os.Exit(1)
}
}

View file

@ -0,0 +1,174 @@
package main
import (
"reflect"
"strings"
"testing"
"github.com/jaffee/commandeer"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/datagen"
pflag13 "github.com/spf13/pflag"
)
func TestDatagenArgs(t *testing.T) {
tests := []struct {
name string
args []string
Source string
Target string
Concurrency int
StartFrom uint64
EndAt uint64
PilosaHosts []string
PilosaIndex string
PilosaBatchSize int
KafkaHosts []string
KafkaRegistryURL string
KafkaTopic string
KafkaSubject string
KafkaBatchSize int
KafkaReplicationFactor int
KafkaNumPartitions int
Datadog bool
}{
{
name: "empty",
args: []string{
"", // os.Args[0] can be ignored
"--source", "example",
},
Source: "example",
Target: "featurebase",
Concurrency: 1,
StartFrom: uint64(0),
EndAt: uint64(0),
PilosaHosts: nil,
PilosaIndex: "",
PilosaBatchSize: 0,
KafkaHosts: nil,
KafkaRegistryURL: "",
KafkaTopic: "",
KafkaSubject: "",
KafkaReplicationFactor: 0,
KafkaNumPartitions: 0,
Datadog: false,
},
{
name: "long",
args: []string{
"datagen",
"--source", "customer",
"--target", "kafka",
"--concurrency", "10",
"--start-from", "1",
"--end-at", "11",
"--pilosa.hosts", "pilosa:1,pilosa:2,pilosa:3",
"--pilosa.index", "idx",
"--pilosa.batch-size", "12345",
"--kafka.confluent-command.kafka-bootstrap-servers", "kafka:1,kafka:2,kafka:3",
"--kafka.confluent-command.schema-registry-url", "registry:1",
"--kafka.subject", "subj",
"--kafka.topic", "top",
"--kafka.batch-size", "2",
"--kafka.replication-factor", "3",
"--kafka.num-partitions", "3",
"--datadog",
},
Source: "customer",
Target: "kafka",
Concurrency: 10,
StartFrom: uint64(1),
EndAt: uint64(11),
PilosaHosts: []string{"pilosa:1", "pilosa:2", "pilosa:3"},
PilosaIndex: "idx",
PilosaBatchSize: 12345,
KafkaHosts: []string{"kafka:1", "kafka:2", "kafka:3"},
KafkaRegistryURL: "registry:1",
KafkaSubject: "subj",
KafkaTopic: "top",
KafkaBatchSize: 2,
KafkaReplicationFactor: 3,
KafkaNumPartitions: 3,
Datadog: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fs := &pflag.FlagSet{FlagSet: pflag13.NewFlagSet(tc.args[0], pflag13.ExitOnError)}
m := datagen.NewMain()
if err := commandeer.LoadArgsEnv(fs, m, tc.args[1:], "GEN_", nil); err != nil {
t.Fatal(err)
}
if err := m.Preload(); err != nil {
t.Fatalf("failed to load: %v", err)
}
if tc.Source != m.Source {
t.Fatalf("--source expected: %v got: %v", tc.Source, m.Source)
}
if tc.Target != m.Target {
t.Fatalf("--target expected: %v got: %v", tc.Target, m.Target)
}
if tc.Concurrency != m.Concurrency {
t.Fatalf("--concurrency expected: %v got: %v", tc.Concurrency, m.Concurrency)
}
if !reflect.DeepEqual(tc.PilosaHosts, m.Pilosa.Hosts) {
t.Fatalf("--pilosa.hosts expected: %v got: %v", tc.PilosaHosts, m.Pilosa.Hosts)
}
if tc.PilosaIndex != m.Pilosa.Index {
t.Fatalf("--pilosa.index expected: %v got: %v", tc.PilosaIndex, m.Pilosa.Index)
}
if tc.PilosaBatchSize != m.Pilosa.BatchSize {
t.Fatalf("--pilosa.batch-size expected: %v got: %v", tc.PilosaBatchSize, m.Pilosa.BatchSize)
}
if tc.KafkaHosts != nil {
bs, err := m.KafkaPut.ConfigMap.Get("bootstrap.servers", nil)
if err != nil || bs == nil {
t.Fatalf("kafka servers not set properly: %v", err)
}
bootstrapServers := bs.(string)
if !reflect.DeepEqual(strings.Join(tc.KafkaHosts, ","), bootstrapServers) {
t.Fatalf("--kafka.hosts expected: %v got: %v", strings.Join(tc.KafkaHosts, ","), bootstrapServers)
}
}
if tc.KafkaRegistryURL != m.Kafka.SchemaRegistryURL {
t.Fatalf("--kafka.registry-url expected: %v got: %v", tc.KafkaRegistryURL, m.Kafka.SchemaRegistryURL)
}
if tc.KafkaSubject != m.Kafka.Subject {
t.Fatalf("--kafka.subject expected: %v got: %v", tc.KafkaSubject, m.Kafka.Subject)
}
if tc.KafkaTopic != m.Kafka.Topic {
t.Fatalf("--kafka.topic expected: %v got: %v", tc.KafkaTopic, m.Kafka.Topic)
}
if tc.KafkaBatchSize != 0 {
if tc.KafkaBatchSize != m.Kafka.BatchSize {
t.Fatalf("--kafka.batch-size expected: %v got: %v", tc.KafkaBatchSize, m.Kafka.BatchSize)
}
}
if tc.KafkaReplicationFactor != 0 {
if tc.KafkaReplicationFactor != m.Kafka.ReplicationFactor {
t.Fatalf("--kafka.replication-factor expected: %v got: %v", tc.KafkaReplicationFactor, m.Kafka.ReplicationFactor)
}
}
if tc.KafkaNumPartitions != 0 {
if tc.KafkaNumPartitions != m.Kafka.NumPartitions {
t.Fatalf("--kafka.num-partitions expected: %v got: %v", tc.KafkaNumPartitions, m.Kafka.NumPartitions)
}
}
})
}
}

View file

@ -0,0 +1,12 @@
## Ingester
_Ingester service_ consumes [Programmatic Ingest API](https://github.com/molecula/docs/blob/master/docs/internal/proposals/programmatic-ingest-api.md)
and sends data to the _Pilosa service_.
```
Usage of ./ingester:
--dry-run Dry run - just flag parsing.
--http-addr string HTTP address for Ingester. (default "localhost:8080")
--pilosa-hosts strings Comma separated list of host:port pairs for Pilosa. (default [localhost:10101])
```

187
idk/cmd/ingester/main.go Normal file
View file

@ -0,0 +1,187 @@
package main
import (
"fmt"
"log"
"net/http"
"runtime/debug"
"github.com/gorilla/mux"
"github.com/jaffee/commandeer/pflag"
pilosaclient "github.com/molecula/featurebase/v3/client"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/api"
"github.com/pkg/errors"
)
func main() {
i := &ingester{
PilosaHosts: []string{"localhost:10101"},
HttpAddr: "localhost:8080",
}
if err := pflag.LoadEnv(i, "INGESTER_", nil); err != nil {
log.Fatal(err)
}
if i.DryRun {
log.Printf("%+v\n", i)
return
}
if err := i.Run(); err != nil {
log.Fatal(err)
}
}
// ingester is a HTTP handler which lets ingest data to pilosa over "programmatic ingest API".
// https://github.com/molecula/docs/blob/master/docs/internal/proposals/programmatic-ingest-api.md
type ingester struct {
DryRun bool `help:"Dry run - just flag parsing."`
PilosaHosts []string `short:"" help:"Comma separated list of host:port pairs for FeatureBase."`
PilosaGrpcHosts []string `short:"" help:"Comma separated list of host:port pairs for FeatureBase's gRPC endpoint."`
BatchSize uint `short:"" help:"Batch size to use for ingest operations."`
HttpAddr string `short:"" help:"HTTP address for Ingester."`
client *pilosaclient.Client
router *mux.Router
tmplMain idk.Main
AuthToken string `flag:"auth-token" help:"Authentication Token for FeatureBase"`
}
func (i *ingester) Run() error {
client, err := pilosaclient.NewClient(i.PilosaHosts,
pilosaclient.OptClientRetries(2),
pilosaclient.OptClientTotalPoolSize(1000),
pilosaclient.OptClientPoolSizePerRoute(400),
)
if err != nil {
return errors.Wrap(err, "getting featurebase client")
}
defer client.Close()
i.client = client
i.router = mux.NewRouter()
i.router.HandleFunc("/schema", i.postSchema).Methods("POST").Name("PostSchema")
i.router.HandleFunc("/index/{index}", i.postIndex).Methods("POST").Name("PostIndex")
m := *idk.NewMain()
m.PilosaHosts = i.PilosaHosts
m.PilosaGRPCHosts = i.PilosaGrpcHosts
m.Pprof = ""
m.Stats = ""
m.PackBools = ""
m.BatchSize = int(i.BatchSize)
m.AuthToken = i.AuthToken
i.tmplMain = m
s := &http.Server{Addr: i.HttpAddr, Handler: i}
return s.ListenAndServe()
}
// ServeHTTP handles an HTTP request.
func (i *ingester) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
stack := debug.Stack()
log.Printf("PANIC: %s\n%s", err, stack)
}
}()
i.router.ServeHTTP(w, r)
}
// POST /schema
func (i *ingester) postSchema(w http.ResponseWriter, r *http.Request) {
body := r.Body
defer body.Close()
schema, ifNotExists, err := api.DecodeSchema(body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
for idxName, idx := range schema.Indexes() {
if err := i.client.CreateIndex(idx); err != nil {
if errors.Is(err, pilosaclient.ErrIndexExists) {
if ifNotExists[idxName] {
continue
}
http.Error(w, err.Error(), http.StatusConflict)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for fldName, fld := range idx.Fields() {
if fldName == "_exists" {
continue
}
if err := i.client.EnsureField(fld); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
// Following code is not really needed,
// but it's kind of informative if we want to check what's the current pilosa schema.
// If we decide to keep it, we'll need to improve "pilosa/client",
// because with current implementation we can serialize just map of indices.
schema, err = i.client.Schema()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(schema.String())) //nolint: errcheck
}
// POST /index/{index}
func (i *ingester) postIndex(w http.ResponseWriter, r *http.Request) {
index := mux.Vars(r)["index"]
schema, err := i.client.Schema()
if err != nil {
http.Error(w, fmt.Sprintf("fetching schema: %v", err), http.StatusInternalServerError)
return
}
idx, ok := schema.Indexes()[index]
if !ok {
http.Error(w, fmt.Sprintf("index %q not found", index), http.StatusNotFound)
return
}
switch ctype := r.Header.Get("Content-Type"); ctype {
case "":
http.Error(w, "missing Content-Type header", http.StatusUnsupportedMediaType)
return
case "application/json":
err = api.IngestJSON(idx, i.tmplMain, r.Body)
if err != nil {
errCode := http.StatusInternalServerError
if errors.As(err, &api.TypeError{}) || errors.As(err, &api.ErrDuplicateElement{}) {
errCode = http.StatusBadRequest
}
http.Error(w, err.Error(), errCode)
return
}
default:
http.Error(w, fmt.Sprintf("unsupported Content-Type %q", ctype), http.StatusUnsupportedMediaType)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK")) //nolint: errcheck
}

30
idk/cmd/kafkagen/main.go Normal file
View file

@ -0,0 +1,30 @@
package main
import (
"log"
"os"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/kafkagen"
"github.com/molecula/featurebase/v3/logger"
)
func main() {
m, err := kafkagen.NewMain()
if err != nil {
log.Fatal(err)
}
if err := pflag.LoadEnv(m, "KGEN_", nil); err != nil {
log.Fatal(err)
}
if m.DryRun {
log.Printf("%+v\n", m)
return
}
if err := m.Run(); err != nil {
logger.NewStandardLogger(os.Stderr).Errorf("Error running command: %v", err)
os.Exit(1)
}
}

View file

@ -0,0 +1,90 @@
package main
import (
"reflect"
"testing"
"github.com/jaffee/commandeer"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/kafkagen"
pflag13 "github.com/spf13/pflag"
)
func TestKafkagenArgs(t *testing.T) {
tests := []struct {
name string
args []string
KafkaHosts []string
RegistryURL string
Topic string
Subject string
SchemaFile string
}{
{
name: "empty",
args: []string{
"",
},
KafkaHosts: []string{"localhost:9092"},
RegistryURL: "localhost:8081",
Topic: "defaulttopic",
Subject: "bigschema",
SchemaFile: "bigschema.json",
},
{
name: "all-set",
args: []string{
"kafkagen",
"--kafka-bootstrap-servers", "kafka:1,kafka:2,kafka:3,kafka4",
"--schema-registry-url", "idk.registry.com",
"--schema-file", "/tmp/schema.json",
"--subject", "whatever",
"--topic", "whatever",
},
KafkaHosts: []string{"kafka:1", "kafka:2", "kafka:3", "kafka4"},
RegistryURL: "idk.registry.com",
Topic: "whatever",
Subject: "whatever",
SchemaFile: "/tmp/schema.json",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fs := &pflag.FlagSet{FlagSet: pflag13.NewFlagSet(tc.args[0], pflag13.ExitOnError)}
m, err := kafkagen.NewMain()
if err != nil {
t.Fatal(err)
}
if err := commandeer.LoadArgsEnv(fs, m, tc.args[1:], "KGEN_", nil); err != nil {
t.Fatal(err)
}
if len(tc.KafkaHosts) > 0 {
if !reflect.DeepEqual(tc.KafkaHosts, m.KafkaBootstrapServers) {
t.Fatalf("--kafka-bootstrap-servers expected: %v got: %v", tc.KafkaHosts, m.KafkaBootstrapServers)
}
}
if tc.RegistryURL != "" {
if !reflect.DeepEqual(tc.RegistryURL, m.SchemaRegistryURL) {
t.Fatalf("--registry-url expected: %v got: %v", tc.RegistryURL, m.SchemaRegistryURL)
}
}
if tc.Topic != "" {
if !reflect.DeepEqual(tc.Topic, m.Topic) {
t.Fatalf("--topic expected: %v got: %v", tc.Topic, m.Topic)
}
}
if tc.Subject != "" {
if !reflect.DeepEqual(tc.Subject, m.Subject) {
t.Fatalf("--subject expected: %v got: %v", tc.Subject, m.Subject)
}
}
})
}
}

34
idk/cmd/kafkaput/main.go Normal file
View file

@ -0,0 +1,34 @@
package main
import (
"log"
"os"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/kafka"
"github.com/molecula/featurebase/v3/logger"
)
func main() {
m, err := kafka.NewPutCmd()
if err != nil {
log.Fatal(err)
}
if err := pflag.LoadEnv(m, "KPUT_", nil); err != nil {
log.Fatal(err)
}
if m.DryRun {
log.Printf("%+v\n", m)
return
}
if err := m.Run(); err != nil {
log := m.Log()
if log == nil {
// if we fail before a logger was instantiated
logger.NewStandardLogger(os.Stderr).Errorf("Error running command: %v", err)
os.Exit(1)
}
log.Errorf("Error running command: %v", err)
os.Exit(1)
}
}

View file

@ -0,0 +1,103 @@
package main
import (
"reflect"
"testing"
"github.com/jaffee/commandeer"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/kafka"
pflag13 "github.com/spf13/pflag"
)
func TestKafkaputArgs(t *testing.T) {
tests := []struct {
name string
args []string
KafkaHosts []string
RegistryURL string
Topic string
Schema string
SchemaFile string
Subject string
Data string
}{
{
name: "empty",
args: []string{
"",
},
KafkaHosts: []string{"localhost:9092"},
RegistryURL: "localhost:8081",
Topic: "defaulttopic",
Subject: "test",
SchemaFile: "",
Schema: "{\"type\": \"record\",\"namespace\": \"c.e\",\"name\": \"F\",\"fields\": [{\"name\":\"id\",\"type\":\"long\"},{\"name\":\"a\",\"type\": \"boolean\"},{\"name\": \"b\", \"type\": \"float\", \"scale\": 2}]}",
Data: "{\"id\": 1, \"a\": true, \"b\": 1.43}",
},
{
name: "all-set",
args: []string{
"kafkaput",
"--kafka-bootstrap-servers", "localhost:0,localhost:80,localhost:9092",
"--schema-registry-url", "https://localhost:8081",
"--topic", "my-topic",
"--schema", "{}",
"--schema-file", "/tmp/schema.json",
"--data", "{}",
"--subject", "my-subject",
},
KafkaHosts: []string{"localhost:0", "localhost:80", "localhost:9092"},
RegistryURL: "https://localhost:8081",
Topic: "my-topic",
Subject: "my-subject",
SchemaFile: "/tmp/schema.json",
Schema: "{}",
Data: "{}",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fs := &pflag.FlagSet{FlagSet: pflag13.NewFlagSet(tc.args[0], pflag13.ExitOnError)}
m, err := kafka.NewPutCmd()
if err != nil {
t.Fatal(err)
}
if err := commandeer.LoadArgsEnv(fs, m, tc.args[1:], "KPUT_", nil); err != nil {
t.Fatal(err)
}
if len(tc.KafkaHosts) > 0 {
if !reflect.DeepEqual(tc.KafkaHosts, m.KafkaBootstrapServers) {
t.Fatalf("--kafka-hosts expected: %v got: %v", tc.KafkaHosts, m.KafkaBootstrapServers)
}
}
if tc.RegistryURL != m.SchemaRegistryURL {
t.Fatalf("--registry-url expected: %v got: %v", tc.RegistryURL, m.SchemaRegistryURL)
}
if tc.Topic != m.Topic {
t.Fatalf("--topic expected: %v got: %v", tc.Topic, m.Topic)
}
if tc.Subject != m.Subject {
t.Fatalf("--subject expected: %v got: %v", tc.Subject, m.Subject)
}
if tc.SchemaFile != m.SchemaFile {
t.Fatalf("--schema-file expected: %v got: %v", tc.SchemaFile, m.SchemaFile)
}
if tc.Schema != m.Schema {
t.Fatalf("--schema expected: %v got: %v", tc.Schema, m.Schema)
}
if tc.Data != m.Data {
t.Fatalf("--data expected: %v got: %v", tc.Data, m.Data)
}
})
}
}

View file

@ -0,0 +1,48 @@
package main
import (
"fmt"
"log"
"os"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/csv"
"github.com/molecula/featurebase/v3/logger"
)
func main() {
m := csv.NewMain()
if err := pflag.LoadEnv(m, "IDKCSV_", nil); err != nil {
log.Fatal(err)
}
m.Rename()
if m.DryRun {
fields, err := m.ValidateHeaders()
if err != nil {
log.Printf("validation error: %v\n", err)
return
}
fmt.Printf("%+v\n", m)
fmt.Printf("Parsed fields:\n")
for _, f := range fields {
fmt.Printf(" %-20T %+[1]v\n", f)
}
return
}
if m.Concurrency != 1 {
m.Log().Infof("Concurrency is not supported for csv ingest. '--concurrency' flag will be ignored.")
m.Concurrency = 1
}
if err := m.Run(); err != nil {
log := m.Log()
if log == nil {
// if we fail before a logger was instantiated
logger.NewStandardLogger(os.Stderr).Errorf("Error running command: %v", err)
os.Exit(1)
}
log.Errorf("Error running command: %v", err)
os.Exit(1)
}
}

View file

@ -0,0 +1,207 @@
package main
import (
"reflect"
"testing"
"github.com/jaffee/commandeer"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/csv"
pflag13 "github.com/spf13/pflag"
)
func TestConsumerCSVArgs(t *testing.T) {
tests := []struct {
name string
args []string
AssumeEmptyPilosa bool
AutoGenerate bool
BatchSize int
Concurrency int
IDField string
PilosaHosts []string
PilosaGRPCHosts []string
Stats string
Verbose bool
ExpSplitBatchMode bool
WriteCSV string
Files []string
Header []string
Index string
JustDoIt bool
IgnoreHeader bool
FutureRename bool
}{
{
name: "empty",
args: []string{
"", // os.Args[0] can be ignored
},
AssumeEmptyPilosa: false,
AutoGenerate: false,
BatchSize: 1,
Concurrency: 1,
IDField: "",
PilosaHosts: []string{"localhost:10101"},
PilosaGRPCHosts: []string{"localhost:20101"},
Stats: "localhost:9093",
Verbose: false,
ExpSplitBatchMode: false,
WriteCSV: "",
Index: "",
JustDoIt: false,
IgnoreHeader: false,
FutureRename: false,
},
{
name: "long",
args: []string{
"molecula-consumer-csv",
"--assume-empty-pilosa", "true",
"--auto-generate", "true",
"--batch-size", "12345",
"--concurrency", "1",
"--exp-split-batch-mode", "true",
"--id-field", "id",
"--index", "index_name",
"--log-path", "/tmp/file.log",
"--pack-bools", "true",
"--pilosa-grpc-hosts", "grpc:1,grpc:2,grpc:3",
"--pilosa-hosts", "pilosa:1,pilosa:2,pilosa:3",
"--primary-key-fields", "primary_key",
"--stats", "localhost:9093",
"--tls.ca-certificate", "/tmp/file.ca",
"--tls.certificate", "/tmp/file.certificate",
"--tls.enable-client-verification", "true",
"--tls.key", "/tmp/file.key",
"--tls.skip-verify", "false",
"--verbose", "true",
"--write-csv", "/tmp/file.csv",
"--files", "f1,f2,f3",
"--header", "h1,h2,h3",
"--index", "index-name",
"--just-do-it", "true",
"--ignore-header", "true",
},
AssumeEmptyPilosa: true,
AutoGenerate: true,
BatchSize: 12345,
Concurrency: 1,
IDField: "id",
PilosaHosts: []string{"pilosa:1", "pilosa:2", "pilosa:3"},
PilosaGRPCHosts: []string{"grpc:1", "grpc:2", "grpc:3"},
Stats: "localhost:9093",
Verbose: true,
ExpSplitBatchMode: true,
WriteCSV: "/tmp/file.csv",
Files: []string{"f1", "f2", "f3"},
Header: []string{"h1", "h2", "h3"},
Index: "index-name",
JustDoIt: true,
IgnoreHeader: true,
FutureRename: false,
},
{
name: "empty",
args: []string{
"molecula-consumer-csv",
"--future.rename", "true",
"--featurebase-hosts", "localhost:50101",
"--featurebase-grpc-hosts", "localhost:60101",
},
AssumeEmptyPilosa: false,
AutoGenerate: false,
BatchSize: 1,
Concurrency: 1,
IDField: "",
PilosaHosts: []string{"localhost:50101"},
PilosaGRPCHosts: []string{"localhost:60101"},
Stats: "localhost:9093",
Verbose: false,
ExpSplitBatchMode: false,
WriteCSV: "",
Index: "",
JustDoIt: false,
IgnoreHeader: false,
FutureRename: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fs := &pflag.FlagSet{FlagSet: pflag13.NewFlagSet(tc.args[0], pflag13.ExitOnError)}
m := csv.NewMain()
if err := commandeer.LoadArgsEnv(fs, m, tc.args[1:], "IDKCSV_", nil); err != nil {
t.Fatal(err)
}
m.Rename()
if tc.AssumeEmptyPilosa != m.AssumeEmptyPilosa {
t.Fatalf("--assume-empty-pilosa expected: %v got: %v", tc.AssumeEmptyPilosa, m.AssumeEmptyPilosa)
}
if tc.AutoGenerate != m.AutoGenerate {
t.Fatalf("--auto-generate expected: %v got: %v", tc.AutoGenerate, m.AutoGenerate)
}
if tc.BatchSize != m.BatchSize {
t.Fatalf("--batch-size expected: %v got: %v", tc.BatchSize, m.BatchSize)
}
if tc.Concurrency != m.Concurrency {
t.Fatalf("--concurrency expected: %v got: %v", tc.Concurrency, m.Concurrency)
}
if tc.IDField != m.IDField {
t.Fatalf("--id-field expected: %v got: %v", tc.IDField, m.IDField)
}
if !reflect.DeepEqual(tc.PilosaHosts, m.PilosaHosts) {
t.Fatalf("--pilosa-hosts expected: %v got: %v", tc.PilosaHosts, m.PilosaHosts)
}
if !reflect.DeepEqual(tc.PilosaGRPCHosts, m.PilosaGRPCHosts) {
t.Fatalf("--pilosa-grpc-hosts expected: %v got: %v", tc.PilosaGRPCHosts, m.PilosaGRPCHosts)
}
if tc.Stats != m.Stats {
t.Fatalf("--stats expected: %v got: %v", tc.Stats, m.Stats)
}
if tc.Verbose != m.Verbose {
t.Fatalf("--verbose expected: %v got: %v", tc.Verbose, m.Verbose)
}
if tc.ExpSplitBatchMode != m.ExpSplitBatchMode {
t.Fatalf("--exp-split-batch-mode expected: %v got: %v", tc.ExpSplitBatchMode, m.ExpSplitBatchMode)
}
if tc.WriteCSV != m.WriteCSV {
t.Fatalf("--write-csv expected: %v got: %v", tc.WriteCSV, m.WriteCSV)
}
if !reflect.DeepEqual(tc.Files, m.Files) {
t.Fatalf("--files expected: %+v got: %+v", tc.Files, m.Files)
}
if !reflect.DeepEqual(tc.Header, m.Header) {
t.Fatalf("--header expected: %v got: %v", tc.Header, m.Header)
}
if tc.Index != m.Index {
t.Fatalf("--index expected: %v got: %v", tc.Index, m.Index)
}
if tc.JustDoIt != m.JustDoIt {
t.Fatalf("--just-do-it expected: %v got: %v", tc.JustDoIt, m.JustDoIt)
}
if tc.IgnoreHeader != m.IgnoreHeader {
t.Fatalf("--ignore-header expected: %v got: %v", tc.IgnoreHeader, m.IgnoreHeader)
}
if tc.FutureRename != m.Future.Rename {
t.Fatalf("--future.rename expected: %v got: %v", tc.FutureRename, m.Future.Rename)
}
source, err := m.NewSource()
// check for err as empty testcase fails to create Source
if err == nil {
csvSource := source.(*csv.Source)
if csvSource.JustDoIt != m.JustDoIt {
t.Fatalf("Source.JustDoIt does not match Main.JustDoIt")
}
if csvSource.IgnoreHeader != m.IgnoreHeader {
t.Fatalf("Source.IgnoreHeader does not match Main.IgnoreHeader")
}
}
})
}
}

View file

@ -0,0 +1,4 @@
### Example
```
./molecula-consumer-github -d id -i events --record-type event --batch-size=100000 --start-time "2015-10-13T00:00:00Z"
```

View file

@ -0,0 +1,435 @@
package main
import (
"compress/gzip"
"context"
"encoding/json"
"expvar"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
_ "net/http/pprof"
"os"
"path/filepath"
"time"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
"github.com/pkg/errors"
)
const (
MinWaitDuration = 5 * time.Minute
)
var (
ghEventsVar = expvar.NewInt("gh_events")
ghEventsPerSecVar = expvar.NewFloat("gh_events_per_sec")
)
func main() {
m := NewMain()
if err := pflag.LoadEnv(m, "IDKGITHUB_", nil); err != nil {
log.Fatal(err)
}
m.Rename()
if m.DryRun {
log.Printf("%+v\n", m)
return
}
if err := m.Run(); err != nil {
log := m.Log()
if log == nil {
// if we fail before a logger was instantiated
logger.NewStandardLogger(os.Stderr).Errorf("Error running command: %v", err)
os.Exit(1)
}
log.Errorf("Error running command: %v", err)
os.Exit(1)
}
}
type Main struct {
idk.Main `flag:"!embed"`
RecordType string `help:"Output record type"`
StartTime time.Time `help:"Start time"`
EndTime time.Time `help:"End time, optional"`
CacheDir string `help:"Directory for local data files"`
}
func NewMain() *Main {
m := &Main{
Main: *idk.NewMain(),
StartTime: time.Now().UTC().Truncate(24 * time.Hour),
}
m.Main.Namespace = "ingester_github"
// Start streaming events to channel in timestamp order.
m.NewSource = func() (idk.Source, error) {
if !IsValidRecordType(m.RecordType) {
return nil, errors.New("invalid record type; must be 'event', 'user', 'issue' or 'repo'")
}
events := make(chan Event)
go m.stream(events)
source := NewSource(events)
source.RecordType = m.RecordType
source.Log = m.Main.Log()
// source.JustDoIt = m.JustDoIt
return source, nil
}
return m
}
func (m *Main) Run() error {
// Set up HTTP endpoint to provide /debug/vars & /debug/pprof
fmt.Println("Serving debug endpoint at http://localhost:7070/debug")
go func() { _ = http.ListenAndServe(":7070", nil) }()
go m.monitor()
return m.Main.Run()
}
func (m *Main) stream(events chan Event) {
defer close(events)
// Ensure start time is aligned to hour.
t := m.StartTime.Truncate(1 * time.Hour)
for {
// Stop once we reach the end time.
if !m.EndTime.IsZero() && t.After(m.EndTime) {
m.Main.Log().Printf("end of time range, stopping")
break
}
// Fetch JSON data and convert to CSV.
// If the data doesn't exist, wait and retry.
if err := m.process(events, t); err == errNotFound {
d := time.Until(t)
if d < MinWaitDuration {
d = MinWaitDuration
}
m.Main.Log().Printf("cannot fetch data for %s, waiting %s", t.Format(time.RFC3339), d)
time.Sleep(d)
continue
} else if err != nil {
m.Main.Log().Printf("error processing data: %s", err)
events <- Event{err: err}
return
}
// Move to the next timestamp.
t = t.Add(time.Hour)
}
}
// process fetches the GitHub Archive gzipped JSON data streams each event to the events channel.
func (m *Main) process(events chan Event, t time.Time) error {
log.Printf("processing %s", t.Format(time.RFC3339))
defer log.Printf("done processing %s", t.Format(time.RFC3339))
// Fetch a reader for the timestamp.
r, err := m.openURLReader(t)
if err != nil {
return err
}
defer r.Close()
// Decompress stream.
gr, err := gzip.NewReader(r)
if err != nil {
return err
}
defer gr.Close()
// Read data as JSON.
dec := json.NewDecoder(gr)
counter := 0
for {
// Unmarshal single event from JSON stream.
var event Event
if err := dec.Decode(&event); err == io.EOF {
return nil
} else if err != nil {
return err
}
if counter%20000 == 0 {
m.Main.Log().Printf("record: id=%d", event.ID)
}
counter++
// Send event to channel.
events <- event
ghEventsVar.Add(1) // total event count
}
}
// monitor runs in a separate goroutine and updates the ingestion rate stats.
func (m *Main) monitor() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
var lastTime time.Time
var lastN int64
for range ticker.C {
now, n := time.Now(), ghEventsVar.Value()
if !lastTime.IsZero() {
ghEventsPerSecVar.Set(float64(lastN-n) / lastTime.Sub(now).Seconds())
}
lastTime, lastN = now, n
}
}
type Source struct {
RecordType string
Log logger.Logger
events chan Event
}
func NewSource(events chan Event) *Source {
return &Source{events: events}
}
func (s *Source) Record() (idk.Record, error) {
for {
event, ok := <-s.events
if !ok {
return nil, io.EOF
}
// Break out the error to hand back to IDK.
if event.err != nil {
return nil, event.err
}
var record idk.Record
switch s.RecordType {
case RecordTypeEvent:
record = EventRecord(event)
case RecordTypeUser:
record = UserRecord(event)
case RecordTypeRepo:
record = RepoRecord(event)
case RecordTypeIssue:
record = IssueRecord(event)
default:
panic(fmt.Errorf("invalid source record type: %q", s.RecordType))
}
// If record implementation a validation function then check it.
// Retry the next record if the record is invalid.
if record, ok := record.(interface {
Valid() bool
}); ok && !record.Valid() {
continue
}
return record, nil
}
}
func (s *Source) Schema() []idk.Field {
switch s.RecordType {
case RecordTypeEvent:
return s.eventSchema()
case RecordTypeUser:
return s.userSchema()
case RecordTypeRepo:
return s.repoSchema()
case RecordTypeIssue:
return s.issueSchema()
default:
panic(fmt.Sprintf("no schema for record type: %q", s.RecordType))
}
}
func (s *Source) eventSchema() []idk.Field {
idMin := int64(0)
idMax := int64(1 << 31)
return []idk.Field{
idk.IDField{NameVal: "id"},
idk.StringField{NameVal: "type"},
idk.IDField{NameVal: "actor_id", Quantum: "YMDH"},
idk.IDField{NameVal: "repo_id", Quantum: "YMDH"},
idk.IntField{NameVal: "actor_bsi", Min: &idMin, Max: &idMax},
idk.IntField{NameVal: "repo_bsi", Min: &idMin, Max: &idMax},
idk.RecordTimeField{NameVal: "created_at", Layout: time.RFC3339},
}
}
func (s *Source) userSchema() []idk.Field {
return []idk.Field{
idk.StringField{NameVal: "login"},
idk.IDField{NameVal: "github_id"},
}
}
func (s *Source) repoSchema() []idk.Field {
return []idk.Field{
idk.StringField{NameVal: "name"},
idk.IDField{NameVal: "github_id"},
}
}
func (s *Source) issueSchema() []idk.Field {
idMin := int64(0)
idMax := int64(1 << 31)
return []idk.Field{
idk.IDField{NameVal: "id"},
idk.StringField{NameVal: "url"},
idk.IntField{NameVal: "number", Min: &idMin, Max: &idMax},
idk.IDField{NameVal: "comments", Mutex: true},
idk.RecordTimeField{NameVal: "created_at", Layout: time.RFC3339},
}
}
func (s *Source) Close() error {
return nil
}
// Output type constants.
const (
RecordTypeEvent = "event"
RecordTypeUser = "user"
RecordTypeRepo = "repo"
RecordTypeIssue = "issue"
)
// IsValidRecordType returns true if s is a valid record type.
func IsValidRecordType(s string) bool {
switch s {
case RecordTypeEvent, RecordTypeUser, RecordTypeRepo, RecordTypeIssue:
return true
default:
return false
}
}
type EventRecord Event
func (r EventRecord) Data() []interface{} {
return []interface{}{r.ID, r.Type, r.Actor.ID, r.Repo.ID, r.Actor.ID, r.Repo.ID, r.CreatedAt}
}
func (r EventRecord) Commit(ctx context.Context) error { return nil }
type UserRecord Event
func (r UserRecord) Data() []interface{} {
return []interface{}{r.Actor.Login, r.Actor.ID}
}
func (r UserRecord) Commit(ctx context.Context) error { return nil }
type RepoRecord Event
func (r RepoRecord) Data() []interface{} {
return []interface{}{r.Repo.Name, r.Repo.ID}
}
func (r RepoRecord) Commit(ctx context.Context) error { return nil }
type IssueRecord Event
func (r IssueRecord) Valid() bool {
return r.Type == "IssuesEvent" || r.Type == "IssueCommentEvent"
}
func (r IssueRecord) Data() []interface{} {
var issue Issue
switch r.Type {
case "IssuesEvent":
var payload IssuePayload
_ = json.Unmarshal(r.Payload, &payload)
issue = payload.Issue
case "IssueCommentEvent":
var payload IssueCommentPayload
_ = json.Unmarshal(r.Payload, &payload)
issue = payload.Issue
}
return []interface{}{issue.ID, issue.URL, issue.Number, issue.Comments, r.CreatedAt}
}
func (r IssueRecord) Commit(ctx context.Context) error { return nil }
func (m *Main) openURLReader(t time.Time) (io.ReadCloser, error) {
filename := fmt.Sprintf("%04d-%02d-%02d-%d.json.gz", t.Year(), t.Month(), t.Day(), t.Hour())
cachePath := filepath.Join(m.CacheDir, filename)
if m.CacheDir != "" {
if fi, err := os.Stat(cachePath); err == nil && fi.Size() != 0 {
m.Log().Printf("using cached file")
return os.Open(cachePath)
}
}
rawurl := "https://data.gharchive.org/" + filename
resp, err := http.Get(rawurl)
if err != nil {
return nil, err
} else if resp.StatusCode == http.StatusNotFound {
resp.Body.Close()
return nil, errNotFound
} else if resp.StatusCode >= 400 {
resp.Body.Close()
return nil, fmt.Errorf("invalid status code: code=%d url=%s", resp.StatusCode, rawurl)
}
// If no cache directory, return body directly.
if m.CacheDir == "" {
return resp.Body, nil
}
// If cache enabled, write to file first and then return.
if buf, err := ioutil.ReadAll(resp.Body); err != nil {
return nil, err
} else if err := ioutil.WriteFile(cachePath+".tmp", buf, 0666); err != nil {
return nil, err
} else if err := os.Rename(cachePath+".tmp", cachePath); err != nil {
return nil, err
}
m.Log().Printf("cached file written")
return os.Open(cachePath)
}
type Event struct {
ID int `json:"id,string"`
Type string `json:"type"`
Actor *Actor `json:"actor"`
Repo *Repo `json:"repo"`
Payload json.RawMessage `json:"payload"`
CreatedAt time.Time `json:"created_at"`
err error
}
type Actor struct {
ID int `json:"id"`
Login string `json:"login"`
}
type Repo struct {
ID int `json:"id"`
Name string `json:"name"`
}
type Issue struct {
ID int `json:"id"`
URL string `json:"url"`
Number int `json:"number"`
Comments int `json:"comments"`
}
type IssuePayload struct {
Issue Issue `json:"issue"`
}
type IssueCommentPayload struct {
Issue Issue `json:"issue"`
}
// errNotFound is returned when a data file does not exist.
var errNotFound = errors.New("not found")

View file

@ -0,0 +1,32 @@
package main
import (
"log"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/kafka"
)
func main() {
dm, err := kafka.NewMain()
if err != nil {
log.Fatal(err)
}
dm.Delete = true
if err := pflag.LoadEnv(dm, "CONSUMER_DEL_", nil); err != nil {
log.Fatal(err)
}
dm.Rename()
if dm.DryRun {
log.Printf("%+v\n", dm)
return
}
if err := dm.Run(); err != nil {
logger := dm.Log()
if logger != nil {
logger.Printf("Error running command: %v", err)
}
log.Fatal(err) // make sure we log to stderr if the error happened before logging was set up
}
}

View file

@ -0,0 +1,192 @@
package main
import (
"reflect"
"testing"
"time"
"github.com/jaffee/commandeer"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/kafka"
pflag13 "github.com/spf13/pflag"
)
func TestConsumerKafkaDeleteArgs(t *testing.T) {
tests := []struct {
name string
args []string
AssumeEmptyPilosa bool
AutoGenerate bool
BatchSize int
Concurrency int
IDField string
PilosaHosts []string
PilosaGRPCHosts []string
Stats string
Verbose bool
ExpSplitBatchMode bool
WriteCSV string
Index string
PrimaryKeyFields []string
Pprof string
KafkaHosts []string
RegistryURL string
MaxMsgs uint64
Group string
Topics []string
Timeout time.Duration
}{
{
name: "empty",
args: []string{
"", // os.Args[0] can be ignored
},
AssumeEmptyPilosa: false,
AutoGenerate: false,
BatchSize: 1,
Concurrency: 1,
IDField: "",
PilosaHosts: []string{"localhost:10101"},
PilosaGRPCHosts: []string{"localhost:20101"},
Stats: "localhost:9093",
Verbose: false,
ExpSplitBatchMode: false,
WriteCSV: "",
Index: "",
Pprof: "localhost:6062",
KafkaHosts: []string{"localhost:9092"},
RegistryURL: "http://localhost:8081",
MaxMsgs: uint64(0),
Group: "defaultgroup",
Topics: []string{"defaulttopic"},
Timeout: time.Second,
},
{
name: "long",
args: []string{
"molecula-consumer-kafka-delete",
"--assume-empty-pilosa", "true",
"--auto-generate", "true",
"--batch-size", "12345",
"--concurrency", "1",
"--exp-split-batch-mode", "true",
"--id-field", "id",
"--index", "index_name",
"--log-path", "/tmp/file.log",
"--pack-bools", "true",
"--pilosa-grpc-hosts", "grpc:1,grpc:2,grpc:3",
"--pilosa-hosts", "pilosa:1,pilosa:2,pilosa:3",
"--stats", "localhost:9093",
"--tls.ca-certificate", "/tmp/file.ca",
"--tls.certificate", "/tmp/file.certificate",
"--tls.enable-client-verification", "true",
"--tls.key", "/tmp/file.key",
"--tls.skip-verify", "false",
"--verbose", "true",
"--write-csv", "/tmp/file.csv",
"--index", "index-name",
"--primary-key-fields", "k1",
"--pprof", "localhost:6666",
"--kafka-bootstrap-servers", "kafka:1,kafka:2,kafka:3",
"--schema-registry-url", "registry.molecula.com",
"--max-msgs", "1234",
"--group", "molecula",
"--topics", "t1,t2,t3,t4",
"--timeout", "123ms",
},
AssumeEmptyPilosa: true,
AutoGenerate: true,
BatchSize: 12345,
Concurrency: 1,
IDField: "id",
PilosaHosts: []string{"pilosa:1", "pilosa:2", "pilosa:3"},
PilosaGRPCHosts: []string{"grpc:1", "grpc:2", "grpc:3"},
Stats: "localhost:9093",
Verbose: true,
ExpSplitBatchMode: true,
WriteCSV: "/tmp/file.csv",
Index: "index-name",
PrimaryKeyFields: []string{"k1"},
KafkaHosts: []string{"kafka:1", "kafka:2", "kafka:3"},
RegistryURL: "registry.molecula.com",
MaxMsgs: uint64(1234),
Group: "molecula",
Topics: []string{"t1", "t2", "t3", "t4"},
Timeout: 123 * time.Millisecond,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fs := &pflag.FlagSet{FlagSet: pflag13.NewFlagSet(tc.args[0], pflag13.ExitOnError)}
m, err := kafka.NewMain()
if err != nil {
t.Fatal(err)
}
m.Delete = true
if err := commandeer.LoadArgsEnv(fs, m, tc.args[1:], "CONSUMER_DEL_", nil); err != nil {
t.Fatal(err)
}
if tc.AssumeEmptyPilosa != m.AssumeEmptyPilosa {
t.Fatalf("--assume-empty-pilosa expected: %v got: %v", tc.AssumeEmptyPilosa, m.AssumeEmptyPilosa)
}
if tc.AutoGenerate != m.AutoGenerate {
t.Fatalf("--auto-generate expected: %v got: %v", tc.AutoGenerate, m.AutoGenerate)
}
if tc.BatchSize != m.BatchSize {
t.Fatalf("--batch-size expected: %v got: %v", tc.BatchSize, m.BatchSize)
}
if tc.Concurrency != m.Concurrency {
t.Fatalf("--concurrency expected: %v got: %v", tc.Concurrency, m.Concurrency)
}
if tc.IDField != m.IDField {
t.Fatalf("--id-field expected: %v got: %v", tc.IDField, m.IDField)
}
if !reflect.DeepEqual(tc.PilosaHosts, m.PilosaHosts) {
t.Fatalf("--pilosa-hosts expected: %v got: %v", tc.PilosaHosts, m.PilosaHosts)
}
if !reflect.DeepEqual(tc.PilosaGRPCHosts, m.PilosaGRPCHosts) {
t.Fatalf("--pilosa-grpc-hosts expected: %v got: %v", tc.PilosaGRPCHosts, m.PilosaGRPCHosts)
}
if tc.Stats != m.Stats {
t.Fatalf("--stats expected: %v got: %v", tc.Stats, m.Stats)
}
if tc.Verbose != m.Verbose {
t.Fatalf("--verbose expected: %v got: %v", tc.Verbose, m.Verbose)
}
if tc.ExpSplitBatchMode != m.ExpSplitBatchMode {
t.Fatalf("--exp-split-batch-mode expected: %v got: %v", tc.ExpSplitBatchMode, m.ExpSplitBatchMode)
}
if tc.WriteCSV != m.WriteCSV {
t.Fatalf("--write-csv expected: %v got: %v", tc.WriteCSV, m.WriteCSV)
}
if tc.Index != m.Index {
t.Fatalf("--index expected: %v got: %v", tc.Index, m.Index)
}
if !reflect.DeepEqual(tc.PrimaryKeyFields, m.PrimaryKeyFields) {
t.Fatalf("--primary-key-fields expected: %v got: %v", tc.PrimaryKeyFields, m.PrimaryKeyFields)
}
if !reflect.DeepEqual(tc.KafkaHosts, m.KafkaBootstrapServers) {
t.Fatalf("--kafka-hosts expected: %v got: %v", tc.KafkaHosts, m.KafkaBootstrapServers)
}
if tc.RegistryURL != m.SchemaRegistryURL {
t.Fatalf("--registry-url expected: %v got: %v", tc.RegistryURL, m.SchemaRegistryURL)
}
if tc.MaxMsgs != m.MaxMsgs {
t.Fatalf("--max-msgs expected: %v got: %v", tc.MaxMsgs, m.MaxMsgs)
}
if tc.Group != m.Group {
t.Fatalf("--group expected: %v got: %v", tc.Group, m.Group)
}
if !reflect.DeepEqual(tc.Topics, m.Topics) {
t.Fatalf("--topics expected: %v got: %v", tc.Topics, m.Topics)
}
if tc.Timeout != m.Timeout {
t.Fatalf("--timeout expected: %v got: %v", tc.Timeout, m.Timeout)
}
})
}
}

View file

@ -0,0 +1,36 @@
package main
import (
"log"
"os"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/kafka_sasl"
"github.com/molecula/featurebase/v3/logger"
)
func main() {
m, err := kafka_sasl.NewMain()
if err != nil {
log.Fatal(err)
}
if err := pflag.LoadEnv(m, "CONSUMER_", nil); err != nil {
log.Fatal(err)
}
m.Rename()
if m.DryRun {
log.Printf("%+v\n", m)
return
}
if err := m.Run(); err != nil {
log := m.Log()
if log == nil {
// if we fail before a logger was instantiated
logger.NewStandardLogger(os.Stderr).Errorf("Error running command: %v", err)
os.Exit(1)
}
log.Errorf("Error running command: %v", err)
os.Exit(1)
}
}

View file

@ -0,0 +1,67 @@
## Molecula Consumer Kafka SASL
#### Background
The goal of **Molecula Consumer Kafka SASL** is to allow IDK to connect to Kafka brokers with SASL-SSL enabled. Currently, the Kafka library we are using in **Molecula Consumer Kafka** and **Molecula Consumer Kafka Static** is Kafka-go from [Segment IO](https://github.com/segmentio/kafka-go) and it does not seem to support both SASL and SSL. After some research, we found that [Confluent Kafka Go](https://github.com/segmentio/kafka-go) is a tool we can use to connect to Kafka brokers with SASL and SSL. **Molecula Consumer Kafka SASL** was written based on **Molecula Consumer Kafka Static**.
#### Future
There should be a ticket coming soon to re-write **Molecula Consumer Kafka** with [Confluent Kafka Go](https://github.com/segmentio/kafka-go). At this moment, one of the challenges that will come up is finding a way to cross compile cgo through different architectures (**arm64!**) [*make docker-release*]. Once **Molecula Consumer Kafka** re-write with [Confluent Kafka Go](https://github.com/segmentio/kafka-go) is done, Kafka-go from [Segment IO](https://github.com/segmentio/kafka-go) should be removed as there is no need to have 2 libraries that basically do the same thing to exist within the project.
#### Kafka SASL
**Molecula Consumer Kafka SASL** should function and behave just like **Molecula Consumer Kafka Static**. The only difference is: **Molecula Consumer Kafka SASL** will be able to connect to Kafka brokers with SASL and SSL enabled by providing the needed information.
#### New options
The following options were added for Kafka SASL. None of the newly added are required.
- Username: SASL authentication username
- Password: SASL authentication password
- IdAlgorithm: The endpoint identification algorithm used by clients to validate server host name. The default value is https.
- SaslMechanism: SASL mechanism to use for authentication. Range: GSSAPI, PLAIN, SCRAM-SHA-256, SCRAM-SHA-512, OAUTHBEARER
- SecurityProtocol: Protocol used to communicate with brokers. Range: plaintext, ssl, sasl_plaintext, sasl_ssl
- SslCaLocation: File or directory path to CA certificate(s) for verifying the broker's key.
- SslCertificateLocation: Path to client's public key (PEM) used for authentication.
- SslKeyLocation: Path to client's private key (PEM) used for authentication.
- SslKeyPassword: Private key passphrase (for use with ssl.key.location and set_ssl_cert()).
#### How to start Kafka SASL and connect to Lccal IDK Broker with SASL and SSL enabled
1. Start Local IDK by navigating to project IDK then enter: ***make shutdown && sudo make startup***
- Looking at **docker-compose.yml**, non SASL & non SSL broker is at kafka:9092
- SASL & SSL broker is at kafka:9094
2. Connect to kafka:9094
- The SASL and SSL information needed to connect to kafka:9094 is under docker-sasl/client-ssl-test.conf
- test connection by listing the topics in kafka:9094:
- kafka-topics --bootstrap-server kafka:9094 --list **--command-config client-ssl-test.conf.conf**
3. Create a new topic to test sending/receiving messages
- kafka-topics --bootstrap-server kafka:9094 --create --replication-factor 1 --partitions 10 **--topic testSasl** --command-config client-ssl-test.conf
4. Create a header file
- Since **Molecula Consumer Kafka SASL** was written based on **Molecula Consumer Kafka Static**, it needs a header file that has a schema inside it
- Example header.json:
```
[
{
"name":"test_string",
"path":[
"s"
],
"type":"string"
}
]
```
- example message that matches the above schema: **{"s":"test 1"}**
5. Create a producer
- kafka-console-producer --bootstrap-server kafka:9094 **--topic testSasl** --producer.config client-ssl-test.conf
6. SSL Certificates:
- There is a script to generate SSL certificates under docker-sasl/ssl_keys/gen-ssl-certs.sh
- Can also use existing certs in the above folder and feed cert files to molecula-consumer-kafka-sasl
- These certs are needed since in this scenario, we are using kafka with SSL enabled on localhost so we have to geenrate self-signed SSL certs.
7. Run molecula-consumer-kafka-sasl
- If binary is not there, build by using **go build**
- Run :
- ./molecula-consumer-kafka-sasl --auto-generate --index test_string --group testGroup --header header.json --topics testSasl --kafka-hosts kafka:9094 --kafka-sasl.username kafkaClient1 --kafka-sasl.password kafkaClient1pw --kafka-sasl.sasl-mechanism PLAIN --kafka-sasl.security-protocol SASL_SSL --kafka-sasl.ssl-ca-location ../../docker-sasl/ssl_keys/ca-cert --kafka-sasl.ssl-certificate-location ../../docker-sasl/ssl_keys/client_kafkaClient_client.pem --kafka-sasl.ssl-key-location ../../docker-sasl/ssl_keys/client_kafkaClient_client.key --kafka-sasl.ssl-key-password 123456
8. Send a message with kafka-console-producer
9. molecula-consumer-kafka-sasl should be able to receive the message and commit it to locally
10. Double check by going to local pilosa (http://127.0.0.1:10101/) -> query builder -> select test_string in the Table dropdown -> Run
- the results should show the messages that were sent by kafka-console-producer

View file

@ -0,0 +1,32 @@
package main
import (
"log"
"os"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/kafka_static"
"github.com/molecula/featurebase/v3/logger"
)
func main() {
m := kafka_static.NewMain()
if err := pflag.LoadEnv(m, "CONSUMER_", nil); err != nil {
log.Fatal(err)
}
m.Rename()
if m.DryRun {
log.Printf("%+v\n", m)
return
}
if err := m.Run(); err != nil {
log := m.Log()
if log == nil {
// if we fail before a logger was instantiated
logger.NewStandardLogger(os.Stderr).Errorf("Error running command: %v", err)
os.Exit(1)
}
log.Errorf("Error running command: %v", err)
os.Exit(1)
}
}

View file

@ -0,0 +1,37 @@
package main
import (
"log"
"os"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/kafka"
"github.com/molecula/featurebase/v3/logger"
)
func main() {
m, err := kafka.NewMain()
if err != nil {
log.Fatal(err)
}
if err := pflag.LoadEnv(m, "CONSUMER_", nil); err != nil {
log.Fatal(err)
}
m.Rename()
if m.DryRun {
log.Printf("%+v\n", m)
return
}
if err := m.Run(); err != nil {
log := m.Log()
if log == nil {
// if we fail before a logger was instantiated
logger.NewStandardLogger(os.Stderr).Errorf("Error running command: %v", err)
os.Exit(1)
}
log.Errorf("Error running command: %v", err)
os.Exit(1)
}
}

View file

@ -0,0 +1,206 @@
package main
import (
"reflect"
"testing"
"time"
"github.com/jaffee/commandeer"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/kafka"
pflag13 "github.com/spf13/pflag"
)
func TestConsumerKafkaArgs(t *testing.T) {
tests := []struct {
name string
args []string
AssumeEmptyPilosa bool
AutoGenerate bool
ExternalGenerate bool
OffsetMode bool
BatchSize int
Concurrency int
IDField string
PilosaHosts []string
PilosaGRPCHosts []string
Stats string
Verbose bool
ExpSplitBatchMode bool
WriteCSV string
Index string
PrimaryKeyFields []string
Pprof string
KafkaHosts []string
RegistryURL string
ConfluentCommand idk.ConfluentCommand
Group string
Topics []string
Timeout time.Duration
}{
{
name: "empty",
args: []string{
"", // os.Args[0] can be ignored
},
AssumeEmptyPilosa: false,
AutoGenerate: false,
ExternalGenerate: false,
OffsetMode: true,
BatchSize: 1,
Concurrency: 1,
IDField: "",
PilosaHosts: []string{"localhost:10101"},
PilosaGRPCHosts: []string{"localhost:20101"},
Stats: "localhost:9093",
Verbose: false,
ExpSplitBatchMode: false,
WriteCSV: "",
Index: "",
Pprof: "localhost:6062",
KafkaHosts: []string{"localhost:9092"},
RegistryURL: "http://localhost:8081",
Group: "defaultgroup",
Topics: []string{"defaulttopic"},
Timeout: time.Second,
},
{
name: "long",
args: []string{
"molecula-consumer-kafka",
"--assume-empty-pilosa", "true",
"--auto-generate", "true",
"--external-generate",
"--batch-size", "12345",
"--concurrency", "1",
"--exp-split-batch-mode", "true",
"--id-field", "id",
"--index", "index_name",
"--kafka-debug", "consumer",
"--log-path", "/tmp/file.log",
"--pack-bools", "true",
"--pilosa-grpc-hosts", "grpc:1,grpc:2,grpc:3",
"--pilosa-hosts", "pilosa:1,pilosa:2,pilosa:3",
"--stats", "localhost:9093",
"--tls.ca-certificate", "/tmp/file.ca",
"--tls.certificate", "/tmp/file.certificate",
"--tls.enable-client-verification", "true",
"--tls.key", "/tmp/file.key",
"--tls.skip-verify", "false",
"--verbose", "true",
"--write-csv", "/tmp/file.csv",
"--index", "index-name",
"--primary-key-fields", "k1",
"--pprof", "localhost:6666",
"--kafka-bootstrap-servers", "kafka:1,kafka:2,kafka:3",
"--schema-registry-url", "registry.molecula.com",
"--group", "molecula",
"--topics", "t1,t2,t3,t4",
"--timeout", "123ms",
},
AssumeEmptyPilosa: true,
ExternalGenerate: true,
AutoGenerate: true,
BatchSize: 12345,
Concurrency: 1,
IDField: "id",
PilosaHosts: []string{"pilosa:1", "pilosa:2", "pilosa:3"},
PilosaGRPCHosts: []string{"grpc:1", "grpc:2", "grpc:3"},
Stats: "localhost:9093",
Verbose: true,
ExpSplitBatchMode: true,
WriteCSV: "/tmp/file.csv",
Index: "index-name",
PrimaryKeyFields: []string{"k1"},
KafkaHosts: []string{"kafka:1", "kafka:2", "kafka:3"},
RegistryURL: "registry.molecula.com",
ConfluentCommand: idk.ConfluentCommand{
KafkaDebug: "consumer",
},
Group: "molecula",
Topics: []string{"t1", "t2", "t3", "t4"},
Timeout: 123 * time.Millisecond,
OffsetMode: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fs := &pflag.FlagSet{FlagSet: pflag13.NewFlagSet(tc.args[0], pflag13.ExitOnError)}
m, err := kafka.NewMain()
if err != nil {
t.Fatal(err)
}
if err := commandeer.LoadArgsEnv(fs, m, tc.args[1:], "CONSUMER_", nil); err != nil {
t.Fatal(err)
}
if tc.AssumeEmptyPilosa != m.AssumeEmptyPilosa {
t.Fatalf("--assume-empty-pilosa expected: %v got: %v", tc.AssumeEmptyPilosa, m.AssumeEmptyPilosa)
}
if tc.AutoGenerate != m.AutoGenerate {
t.Fatalf("--auto-generate expected: %v got: %v", tc.AutoGenerate, m.AutoGenerate)
}
if tc.ExternalGenerate != m.ExternalGenerate {
t.Fatalf("--external-generate expected: %v got: %v", tc.ExternalGenerate, m.ExternalGenerate)
}
if tc.OffsetMode != m.OffsetMode {
t.Fatalf("--offset-mode expected: %v got: %v", tc.OffsetMode, m.OffsetMode)
}
if tc.BatchSize != m.BatchSize {
t.Fatalf("--batch-size expected: %v got: %v", tc.BatchSize, m.BatchSize)
}
if tc.Concurrency != m.Concurrency {
t.Fatalf("--concurrency expected: %v got: %v", tc.Concurrency, m.Concurrency)
}
if tc.IDField != m.IDField {
t.Fatalf("--id-field expected: %v got: %v", tc.IDField, m.IDField)
}
if !reflect.DeepEqual(tc.PilosaHosts, m.PilosaHosts) {
t.Fatalf("--pilosa-hosts expected: %v got: %v", tc.PilosaHosts, m.PilosaHosts)
}
if !reflect.DeepEqual(tc.PilosaGRPCHosts, m.PilosaGRPCHosts) {
t.Fatalf("--pilosa-grpc-hosts expected: %v got: %v", tc.PilosaGRPCHosts, m.PilosaGRPCHosts)
}
if tc.Stats != m.Stats {
t.Fatalf("--stats expected: %v got: %v", tc.Stats, m.Stats)
}
if tc.Verbose != m.Verbose {
t.Fatalf("--verbose expected: %v got: %v", tc.Verbose, m.Verbose)
}
if tc.ExpSplitBatchMode != m.ExpSplitBatchMode {
t.Fatalf("--exp-split-batch-mode expected: %v got: %v", tc.ExpSplitBatchMode, m.ExpSplitBatchMode)
}
if tc.WriteCSV != m.WriteCSV {
t.Fatalf("--write-csv expected: %v got: %v", tc.WriteCSV, m.WriteCSV)
}
if tc.Index != m.Index {
t.Fatalf("--index expected: %v got: %v", tc.Index, m.Index)
}
if !reflect.DeepEqual(tc.PrimaryKeyFields, m.PrimaryKeyFields) {
t.Fatalf("--primary-key-fields expected: %v got: %v", tc.PrimaryKeyFields, m.PrimaryKeyFields)
}
if !reflect.DeepEqual(tc.KafkaHosts, m.KafkaBootstrapServers) {
t.Fatalf("--kafka-hosts expected: %v got: %v", tc.KafkaHosts, m.KafkaBootstrapServers)
}
if tc.RegistryURL != m.SchemaRegistryURL {
t.Fatalf("--registry-url expected: %v got: %v", tc.RegistryURL, m.SchemaRegistryURL)
}
if tc.ConfluentCommand.KafkaDebug != m.ConfluentCommand.KafkaDebug {
t.Fatalf("--kafka-debug expected: %v got: %v", tc.ConfluentCommand.KafkaDebug, m.ConfluentCommand.KafkaDebug)
}
if tc.Group != m.Group {
t.Fatalf("--group expected: %v got: %v", tc.Group, m.Group)
}
if !reflect.DeepEqual(tc.Topics, m.Topics) {
t.Fatalf("--topics expected: %v got: %v", tc.Topics, m.Topics)
}
if tc.Timeout != m.Timeout {
t.Fatalf("--timeout expected: %v got: %v", tc.Timeout, m.Timeout)
}
})
}
}

View file

@ -0,0 +1,32 @@
package main
import (
"log"
"os"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/kinesis"
"github.com/molecula/featurebase/v3/logger"
)
func main() {
m := kinesis.NewMain()
if err := pflag.LoadEnv(m, "CONSUMER_", nil); err != nil {
log.Fatal(err)
}
m.Rename()
if m.DryRun {
log.Printf("%+v\n", m)
return
}
if err := m.Run(); err != nil {
log := m.Log()
if log == nil {
// if we fail before a logger was instantiated
logger.NewStandardLogger(os.Stderr).Errorf("Error running command: %v", err)
os.Exit(1)
}
log.Errorf("Error running command: %v", err)
os.Exit(1)
}
}

View file

@ -0,0 +1,38 @@
package main
import (
"log"
"os"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/sql"
"github.com/molecula/featurebase/v3/logger"
)
func main() {
m := sql.NewMain()
if err := pflag.LoadEnv(m, "IDK_", nil); err != nil {
log.Fatal(err)
}
m.Rename()
if m.DryRun {
log.Printf("%+v\n", m)
return
}
if m.Concurrency != 1 {
m.Log().Infof("Concurrency is not supported for sql ingest. '--concurrency' flag will be ignored.")
m.Concurrency = 1
}
if err := m.Run(); err != nil {
log := m.Log()
if log == nil {
// if we fail before a logger was instantiated
logger.NewStandardLogger(os.Stderr).Errorf("Error running command: %v", err)
os.Exit(1)
}
log.Errorf("Error running command: %v", err)
os.Exit(1)
}
}

View file

@ -0,0 +1,151 @@
package main
import (
"reflect"
"testing"
"github.com/jaffee/commandeer"
"github.com/jaffee/commandeer/pflag"
"github.com/molecula/featurebase/v3/idk/sql"
pflag13 "github.com/spf13/pflag"
)
func TestConsumerSQLArgs(t *testing.T) {
tests := []struct {
name string
args []string
AssumeEmptyPilosa bool
AutoGenerate bool
BatchSize int
Concurrency int
IDField string
PilosaHosts []string
PilosaGRPCHosts []string
Stats string
Verbose bool
ExpSplitBatchMode bool
WriteCSV string
Driver string
ConnectionString string
RowExpr string
StringArraySeparator string
}{
{
name: "empty",
args: []string{
"", // os.Args[0] can be ignored
},
BatchSize: 1,
Concurrency: 1,
PilosaHosts: []string{"localhost:10101"},
PilosaGRPCHosts: []string{"localhost:20101"},
Stats: "localhost:9093",
Driver: "postgres",
ConnectionString: "postgres://user:password@localhost:5432/defaultindex?sslmode=disable",
StringArraySeparator: ",",
},
{
name: "long",
args: []string{
"molecula-consumer-sql",
"--assume-empty-pilosa", "true",
"--auto-generate", "true",
"--batch-size", "12345",
"--concurrency", "1",
"--exp-split-batch-mode", "true",
"--id-field", "id",
"--index", "index_name",
"--log-path", "/tmp/file.log",
"--pack-bools", "true",
"--pilosa-grpc-hosts", "grpc:1,grpc:2,grpc:3",
"--pilosa-hosts", "pilosa:1,pilosa:2,pilosa:3",
"--primary-key-fields", "primary_key",
"--stats", "localhost:9093",
"--tls.ca-certificate", "/tmp/file.ca",
"--tls.certificate", "/tmp/file.certificate",
"--tls.enable-client-verification", "true",
"--tls.key", "/tmp/file.key",
"--tls.skip-verify", "false",
"--verbose", "true",
"--write-csv", "/tmp/file.csv",
"--driver", "mysql",
"--connection-string", "mysql://user:password@localhost:3306/index_name",
"--string-array-separator", ";",
"--row-expr", "select 1;",
},
AssumeEmptyPilosa: true,
AutoGenerate: true,
BatchSize: 12345,
Concurrency: 1,
IDField: "id",
PilosaHosts: []string{"pilosa:1", "pilosa:2", "pilosa:3"},
PilosaGRPCHosts: []string{"grpc:1", "grpc:2", "grpc:3"},
Stats: "localhost:9093",
Verbose: true,
ExpSplitBatchMode: true,
WriteCSV: "/tmp/file.csv",
Driver: "mysql",
ConnectionString: "mysql://user:password@localhost:3306/index_name",
StringArraySeparator: ";",
RowExpr: "select 1;",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fs := &pflag.FlagSet{FlagSet: pflag13.NewFlagSet(tc.args[0], pflag13.ExitOnError)}
m := sql.NewMain()
if err := commandeer.LoadArgsEnv(fs, m, tc.args[1:], "IDK_", nil); err != nil {
t.Fatal(err)
}
if tc.AssumeEmptyPilosa != m.AssumeEmptyPilosa {
t.Fatalf("--assume-empty-pilosa expected: %v got: %v", tc.AssumeEmptyPilosa, m.AssumeEmptyPilosa)
}
if tc.AutoGenerate != m.AutoGenerate {
t.Fatalf("--auto-generate expected: %v got: %v", tc.AutoGenerate, m.AutoGenerate)
}
if tc.BatchSize != m.BatchSize {
t.Fatalf("--batch-size expected: %v got: %v", tc.BatchSize, m.BatchSize)
}
if tc.Concurrency != m.Concurrency {
t.Fatalf("--concurrency expected: %v got: %v", tc.Concurrency, m.Concurrency)
}
if tc.IDField != m.IDField {
t.Fatalf("--id-field expected: %v got: %v", tc.IDField, m.IDField)
}
if !reflect.DeepEqual(tc.PilosaHosts, m.PilosaHosts) {
t.Fatalf("--pilosa-hosts expected: %v got: %v", tc.PilosaHosts, m.PilosaHosts)
}
if !reflect.DeepEqual(tc.PilosaGRPCHosts, m.PilosaGRPCHosts) {
t.Fatalf("--pilosa-grpc-hosts expected: %v got: %v", tc.PilosaGRPCHosts, m.PilosaGRPCHosts)
}
if tc.Stats != m.Stats {
t.Fatalf("--stats expected: %v got: %v", tc.Stats, m.Stats)
}
if tc.Verbose != m.Verbose {
t.Fatalf("--verbose expected: %v got: %v", tc.Verbose, m.Verbose)
}
if tc.ExpSplitBatchMode != m.ExpSplitBatchMode {
t.Fatalf("--exp-split-batch-mode expected: %v got: %v", tc.ExpSplitBatchMode, m.ExpSplitBatchMode)
}
if tc.WriteCSV != m.WriteCSV {
t.Fatalf("--write-csv expected: %v got: %v", tc.WriteCSV, m.WriteCSV)
}
if tc.Driver != m.Driver {
t.Fatalf("--driver expected: %v got: %v", tc.Driver, m.Driver)
}
if tc.ConnectionString != m.ConnectionString {
t.Fatalf("--connection-string expected: %v got: %v", tc.ConnectionString, m.ConnectionString)
}
if tc.StringArraySeparator != m.StringArraySeparator {
t.Fatalf("--string-array-separator expected: %v got: %v", tc.StringArraySeparator, m.StringArraySeparator)
}
if tc.RowExpr != m.RowExpr {
t.Fatalf("--row-expr expected: %v got: %v", tc.RowExpr, m.RowExpr)
}
})
}
}

191
idk/common/configMap.go Normal file
View file

@ -0,0 +1,191 @@
package common
import (
"encoding/json"
"fmt"
"io/ioutil"
"strings"
"sync/atomic"
confluent "github.com/confluentinc/confluent-kafka-go/kafka"
"github.com/molecula/featurebase/v3/idk"
"github.com/pkg/errors"
)
// LaunchKafkaEventConfirmer consumes the events channel for the messages Produced by the producer
func LaunchKafkaEventConfirmer(producer *confluent.Producer, finished *int32, iter *int64) chan struct{} {
doneChan := make(chan struct{})
go func() {
defer func() {
close(doneChan)
}()
cnt := int64(0)
for e := range producer.Events() {
switch ev := e.(type) {
case *confluent.Message:
cnt += 1
msg := ev
if msg.TopicPartition.Error != nil {
fmt.Printf("Delivery failed: %v\n", msg.TopicPartition.Error)
}
default:
fmt.Printf("Ignored event: %s\n", ev)
}
check := atomic.LoadInt32(finished)
if check > 0 {
sent := atomic.LoadInt64(iter)
if cnt >= sent {
doneChan <- struct{}{}
}
}
}
}()
return doneChan
}
func SetupConfluent(m *idk.ConfluentCommand) (*confluent.ConfigMap, error) {
var err error
configMap := &confluent.ConfigMap{}
if m.KafkaConfiguration != "" {
file, er := ioutil.ReadFile(m.KafkaConfiguration)
if er != nil {
return nil, er
}
er = json.Unmarshal([]byte(file), configMap)
if er != nil {
return nil, er
}
}
err = configMap.SetKey("enable.auto.commit", false)
if err != nil {
return nil, errors.Wrap(err, "disabling auto commit")
}
if m.KafkaSocketTimeoutMs > 0 {
err = configMap.SetKey("socket.timeout.ms", m.KafkaSocketTimeoutMs)
if err != nil {
return nil, errors.Wrap(err, "setting socket timeout")
}
} else {
err = configMap.SetKey("socket.timeout.ms", 60000)
if err != nil {
return nil, errors.Wrap(err, "setting socket timeout")
}
}
if len(m.KafkaBootstrapServers) > 0 {
err = configMap.SetKey("bootstrap.servers", strings.Join(m.KafkaBootstrapServers, ","))
if err != nil {
return nil, errors.Wrap(err, "setting bootstrap")
}
} else {
err = configMap.SetKey("bootstrap.servers", "localhost:9092")
if err != nil {
return nil, errors.Wrap(err, "setting bootstrap")
}
}
//SSL
if m.KafkaSslCaLocation != "" {
err = configMap.SetKey("ssl.ca.location", m.KafkaSslCaLocation)
if err != nil {
return nil, err
}
}
if m.KafkaSslKeyLocation != "" {
err = configMap.SetKey("ssl.key.location", m.KafkaSslKeyLocation)
if err != nil {
return nil, err
}
}
if m.KafkaSslKeyPassword != "" {
err = configMap.SetKey("ssl.key.password", m.KafkaSslKeyPassword)
if err != nil {
return nil, err
}
}
if m.KafkaSslCertificateLocation != "" {
err = configMap.SetKey("ssl.certificate.location", m.KafkaSslCertificateLocation)
if err != nil {
return nil, err
}
}
err = configMap.SetKey("enable.ssl.certificate.verification", m.KafkaEnableSslCertificateVerification)
if err != nil {
return nil, err
}
if m.KafkaSslEndpointIdentificationAlgorithm != "" {
err = configMap.SetKey("ssl.endpoint.identification.algorithm", m.KafkaSslEndpointIdentificationAlgorithm)
if err != nil {
return nil, err
}
}
//SSL
if m.KafkaSslCaLocation != "" {
err = configMap.SetKey("ssl.ca.location", m.KafkaSslCaLocation)
if err != nil {
return nil, err
}
}
if m.KafkaSslKeyLocation != "" {
err = configMap.SetKey("ssl.key.location", m.KafkaSslKeyLocation)
if err != nil {
return nil, err
}
}
if m.KafkaSslCertificateLocation != "" {
err = configMap.SetKey("ssl.certificate.location", m.KafkaSslCertificateLocation)
if err != nil {
return nil, err
}
}
err = configMap.SetKey("enable.ssl.certificate.verification", m.KafkaEnableSslCertificateVerification)
if err != nil {
return nil, err
}
if m.KafkaSslEndpointIdentificationAlgorithm != "" {
err = configMap.SetKey("ssl.endpoint.identification.algorithm", m.KafkaSslEndpointIdentificationAlgorithm)
if err != nil {
return nil, err
}
}
// SASL,
if m.KafkaSaslUsername != "" && m.KafkaSaslPassword != "" {
err = configMap.SetKey("sasl.username", m.KafkaSaslUsername)
if err != nil {
return nil, err
}
err = configMap.SetKey("sasl.password", m.KafkaSaslPassword)
if err != nil {
return nil, err
}
}
if m.KafkaSaslMechanism != "" {
err = configMap.SetKey("sasl.mechanism", m.KafkaSaslMechanism)
if err != nil {
return nil, err
}
}
if m.KafkaSecurityProtocol != "" {
err = configMap.SetKey("security.protocol", m.KafkaSecurityProtocol)
if err != nil {
return nil, err
}
}
if m.KafkaClientId != "" {
err = configMap.SetKey("client.id", m.KafkaClientId)
if err != nil {
return nil, err
}
}
if m.KafkaDebug != "" {
err = configMap.SetKey("debug", m.KafkaDebug)
if err != nil {
return nil, err
}
}
return configMap, nil
}

138
idk/csv/cmd.go Normal file
View file

@ -0,0 +1,138 @@
package csv
import (
"encoding/csv"
"os"
"path/filepath"
"sync"
"github.com/molecula/featurebase/v3/idk"
"github.com/pkg/errors"
)
type Main struct {
idk.Main `flag:"!embed"`
Files []string `help:"List of files, URLs, or directories to ingest."`
Header []string `help:"Optional header. If not passed, first line of each file is used."`
IgnoreHeader bool `short:"g" help:"Ignore header in file and use configured header. You *must* configure a header."`
JustDoIt bool `short:"j" help:"Any header field not in the appropriate format, just downcase, use it as the name and process the value as a String/set field"`
files chan string
visited map[string]struct{}
}
func NewMain() *Main {
m := &Main{
Main: *idk.NewMain(),
files: make(chan string),
visited: make(map[string]struct{}),
}
m.Main.Namespace = "ingester_csv"
once := &sync.Once{}
m.NewSource = func() (idk.Source, error) {
if len(m.Files) == 0 {
return nil, errors.New("must provide at least one file or directory with --files")
}
once.Do(func() { go m.streamFileNames() })
source := NewSource()
source.Files = m.files
source.Header = m.Header
source.Log = m.Main.Log()
source.once = &sync.Once{}
source.JustDoIt = m.JustDoIt
source.IgnoreHeader = m.IgnoreHeader
return source, nil
}
return m
}
// ValidateHeaders is used during --dry-run to check that all headers provided at CLI or in
// CSV files can be parsed properly.
// Also returns the field structs as it would be parsed in a normal run. In the case of
// multiple CSV files, only returns the fields for the last file.
func (m *Main) ValidateHeaders() ([]idk.Field, error) {
var err error
fields := make([]idk.Field, 0)
if len(m.Header) > 0 {
fields, err = m.validateHeader(m.Header)
if err != nil {
return fields, errors.Wrap(err, "validating header")
}
} else {
for _, f := range m.Files {
f, err := openFileOrURL(f)
if err != nil {
return fields, errors.Wrapf(err, "opening file (%s)", f)
}
defer f.Close()
reader := csv.NewReader(f)
header, err := reader.Read()
if err != nil {
return fields, errors.Wrapf(err, "reading CSV file (%s)", f)
}
fields, err = m.validateHeader(header)
if err != nil {
return fields, errors.Wrap(err, "validating header")
}
}
}
return fields, nil
}
func (m *Main) validateHeader(h []string) ([]idk.Field, error) {
fields := make([]idk.Field, 0)
for _, val := range h {
field, err := idk.HeaderToField(val, m.Log())
if err != nil {
return fields, errors.Wrapf(err, "invalid header (%v)", val)
}
fields = append(fields, field)
}
return fields, nil
}
func (m *Main) streamFileNames() {
defer close(m.files)
for _, filename := range m.Files {
if err := filepath.Walk(filename, m.traverse); err != nil {
m.Log().Printf("filepath.Walk(%s) %v", filename, err)
continue
}
}
}
func (m *Main) traverse(path string, fi os.FileInfo, err error) error {
if err != nil {
m.Log().Printf("prevent panic by handling failure accessing a path %q: %v\n", path, err)
return err
}
switch mode := fi.Mode(); {
case mode.IsRegular():
if _, ok := m.visited[path]; !ok {
m.visited[path] = struct{}{}
m.files <- path
}
case mode.IsDir():
if _, ok := m.visited[path]; !ok {
m.visited[path] = struct{}{}
}
case mode&os.ModeSymlink != 0:
lnk, err := filepath.EvalSymlinks(path)
if err != nil {
return err
}
if _, ok := m.visited[lnk]; !ok {
if err := filepath.Walk(lnk, m.traverse); err != nil {
return err
}
}
}
return nil
}

654
idk/csv/csv_test.go Normal file
View file

@ -0,0 +1,654 @@
package csv
import (
"fmt"
"io"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"testing"
"time"
"github.com/molecula/featurebase/v3/idk/idktest"
)
func configureTestFlags(main *Main) {
if pilosaHost, ok := os.LookupEnv("IDK_TEST_PILOSA_HOST"); ok {
main.PilosaHosts = []string{pilosaHost}
} else {
main.PilosaHosts = []string{"pilosa:10101"}
}
if grpcHost, ok := os.LookupEnv("IDK_TEST_GRPC_HOST"); ok {
main.PilosaGRPCHosts = []string{grpcHost}
} else {
main.PilosaGRPCHosts = []string{"pilosa:20101"}
}
main.Stats = ""
}
func TestCSVCommand(t *testing.T) {
file := `
id__ID,s__String_F_YMDH,__RecordTime_2006-01-02T15
0,a,2019-01-09T04
1,a,2019-01-09T05
2,a,2019-02-09T04
3,b,2019-01-09T04
4,a,2018-01-09T04
5,a,2019-01-10T04
`[1:]
name := writeTempFile(t, file)
m := NewMain()
m.AutoGenerate = true
configureTestFlags(m)
m.Files = []string{name}
rand.Seed(time.Now().UnixNano())
m.Index = fmt.Sprintf("csvtest%d", rand.Intn(100000))
defer func() {
if err := m.PilosaClient().DeleteIndexByName(m.Index); err != nil {
t.Logf("deleting test index: %v", err)
}
}()
err := m.Run()
if err != nil {
t.Fatalf("running: %v", err)
}
client := m.PilosaClient()
schema, err := client.Schema()
if err != nil {
t.Fatalf("getting schema: %v", err)
}
index := schema.Index(m.Index)
s := index.Field("s")
resp, err := client.Query(s.Range("a", tim(t, "2019-01-07T03"), tim(t, "2019-01-10T05")))
if err != nil {
t.Fatalf("querying: %v", err)
}
if !reflect.DeepEqual(resp.Results()[0].Row().Columns, []uint64{0, 1, 5}) {
t.Errorf("got/exp\n%v\n%v", resp.Results()[0].Row().Columns, []uint64{0, 1, 5})
}
}
func TestCSVCommandCustomHeader(t *testing.T) {
file := `
ABCD,2019-01-02,70%
ABCD,2019-01-03,20%
ABCD,2019-01-04,30%
BEDF,2019-01-02,70%
BEDF,2019-01-05,90%
BEDF,2019-01-08,10%
BEDF,2019-01-08,20%
ABCD,2019-01-30,40%
`[1:]
name := writeTempFile(t, file)
m := NewMain()
m.AutoGenerate = true
m.Header = []string{"asset_tag__String", "fan_time__RecordTime_2006-01-02"}
configureTestFlags(m)
m.Files = []string{name}
rand.Seed(time.Now().UnixNano())
m.Index = fmt.Sprintf("csvtest%d", rand.Intn(100000))
defer func() {
if err := m.PilosaClient().DeleteIndexByName(m.Index); err != nil {
t.Logf("deleting test index: %v", err)
}
}()
err := m.Run()
if err != nil {
t.Fatalf("running: %v", err)
}
client := m.PilosaClient()
schema, err := client.Schema()
if err != nil {
t.Fatalf("getting schema: %v", err)
}
index := schema.Index(m.Index)
s := index.Field("asset_tag")
resp, err := client.Query(s.Row("ABCD"))
if err != nil {
t.Fatalf("querying: %v", err)
}
if !reflect.DeepEqual(resp.Results()[0].Row().Columns, []uint64{0, 1, 2, 7}) {
t.Errorf("got/exp\n%v\n%v", resp.Results()[0].Row().Columns, []uint64{0, 1, 2, 7})
}
}
type recordTimeTestCase struct {
name string
layout string
unit string
scale int // how many unit it takes to move an hour
}
func TestCSVRecordTime(t *testing.T) {
testCases := []recordTimeTestCase{
{name: "plusHours", layout: "", unit: "h", scale: 1},
{name: "plusMinutes", layout: "", unit: "m", scale: 60},
{name: "plusSeconds", layout: "", unit: "s", scale: 60 * 60},
}
// We set our epoch at 4AM, 2019-01-09, because that's the epoch used in
// the previous example.
arbitraryEpoch, err := time.Parse("2006-01-02T15", "2019-01-09T04")
if err != nil {
t.Fatalf("parsing time: %v", err)
}
for _, tc := range testCases {
lines := []string{}
layout := tc.layout
if layout == "" {
layout = time.RFC3339
}
header := fmt.Sprintf(`id__ID,s__String_F_YMDH,__RecordTime_%s_%s_%s`,
layout, arbitraryEpoch.Format(layout), tc.unit)
lines = append(lines, header)
// We expect -1..1 to show up in our query, and -2 and +2 not to.
for i, offset := range []int{-2, -1, 0, 1, 2} {
record := fmt.Sprintf("%d,a,%d", i, offset*tc.scale)
lines = append(lines, record)
}
lines = append(lines, "")
file := strings.Join(lines, "\n")
name := writeTempFile(t, file)
m := NewMain()
m.AutoGenerate = true
configureTestFlags(m)
m.Files = []string{name}
rand.Seed(time.Now().UnixNano())
m.Index = fmt.Sprintf("csvtest%d", rand.Intn(100000))
defer func() {
if err := m.PilosaClient().DeleteIndexByName(m.Index); err != nil {
t.Logf("deleting test index: %v", err)
}
}()
err := m.Run()
if err != nil {
t.Fatalf("running: %v", err)
}
client := m.PilosaClient()
schema, err := client.Schema()
if err != nil {
t.Fatalf("getting schema: %v", err)
}
index := schema.Index(m.Index)
s := index.Field("s")
// We specify a range of one minute after the hour because otherwise
// we hit the end of the range and don't count the +1 case. I am
// not convinced that this is correct.
resp, err := client.Query(s.Range("a", tim(t, "2019-01-09T03"), tim(t, "2019-01-09T05").Add(1*time.Minute)))
if err != nil {
t.Fatalf("querying: %v", err)
}
if !reflect.DeepEqual(resp.Results()[0].Row().Columns, []uint64{1, 2, 3}) {
t.Errorf("record time (%s): got/exp\n%v\n%v", tc.name, resp.Results()[0].Row().Columns, []uint64{1, 2, 3})
}
}
}
func tim(t *testing.T, tstr string) time.Time {
ti, err := time.Parse("2006-01-02T15", tstr)
if err != nil {
t.Fatalf("parsing time: %v", err)
}
return ti
}
func writeTempFile(t *testing.T, data string) string {
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatalf("getting temp file: %v", err)
}
defer f.Close()
_, err = io.WriteString(f, data)
if err != nil {
t.Fatalf("writing string: %v", err)
}
return f.Name()
}
func TestStreamFileNames(t *testing.T) {
tmp, err := testDirTree("d1/d2/d3/d4/d5")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)
mtx := &sync.Mutex{}
visited := make(map[string]int)
m := NewMain()
configureTestFlags(m)
// duplicate all files from the same directory
m.Files = []string{tmp, tmp, tmp, tmp, tmp, tmp}
_, err = m.NewSource()
if err != nil {
t.Fatal(err)
}
wg := &sync.WaitGroup{}
visitor := func(files chan string) {
defer wg.Done()
for f := range files {
// count all visited files
mtx.Lock()
visited[f]++
mtx.Unlock()
}
}
wg.Add(3)
go visitor(m.files)
go visitor(m.files)
go visitor(m.files)
wg.Wait()
for p, n := range visited {
fi, err := os.Lstat(p)
if err != nil {
t.Fatal(err)
}
if fi.Mode().IsRegular() && n != 1 {
t.Fatalf("file %s was visited %d times", p, n)
}
}
}
func testDirTree(tree string) (string, error) {
tmp, err := ioutil.TempDir("", "")
if err != nil {
return "", err
}
err = os.MkdirAll(filepath.Join(tmp, tree), 0755)
if err != nil {
os.RemoveAll(tmp)
return "", err
}
return tmp, filepath.Walk(tmp, func(path string, _ os.FileInfo, _ error) error {
f, err := os.Create(filepath.Join(path, "csv"))
if err != nil {
return err
}
return f.Close()
})
}
// Tests various out of range values are ingested as nil when CLI flags are set to true.
func TestVariousOORValues(t *testing.T) {
file := `
id__ID,s__String_F_YMDH,ts__Timestamp_s_2006-01-02 15:04:05.999,price__Decimal_2,age__Int_1_120
0,a,1832-01-03 08:00:00.000,0.0,1
1,b,1700-01-03 08:00:00.000,5.44,35
2,b,2019-50-03 08:00:00.000,5.44,120
3,b,2019-01-50 08:00:00.000,5.44,120
4,b,2019-01-03 50:00:00.000,5.44,120
5,a,2019-04-03 00:90:00.000,5.44,129
6,a,2019-04-03 00:00:90.000,5.44,120
5,a,2019-04-03 00:00:00.000,123.123,1
6,a,2019-04-03 00:00:00.000,-1,1
7,a,2019-04-03 00:00:00.000,994492233720368547758.0892233720368547758,100
5,a,2019-04-03 00:00:00.000,2.34,121
6,a,2019-04-03 00:00:00.000,3.44,0
7,a,1500-04-03 00:00:00.000,994492233720368547758.0892233720368547758,2342342
8,a,2019-04-03 00:00:00.000,3.44,100
`[1:]
name := writeTempFile(t, file)
checker := make(map[string]interface{})
checker["ts"] = []interface{}{nil, nil, nil, nil, nil, nil, nil, "2019-04-03T00:00:00Z", "2019-04-03T00:00:00Z", "2019-04-03T00:00:00Z", "2019-04-03T00:00:00Z", "2019-04-03T00:00:00Z", nil, "2019-04-03T00:00:00Z"}
checker["age"] = []interface{}{1, 35, 120, 120, 120, nil, 120, 1, 1, 100, nil, nil, nil, 100}
checker["price"] = []interface{}{0.0, 5.44, 5.44, 5.44, 5.44, 5.44, 5.44, 123.12, -1.0, nil, 2.34, 3.44, nil, 3.44}
m := NewMain()
m.AutoGenerate = true
m.BatchSize = 1
configureTestFlags(m)
m.Files = []string{name}
rand.Seed(time.Now().UnixNano())
m.Index = fmt.Sprintf("csvtest%d", rand.Intn(100000))
m.AllowIntOutOfRange = true
m.AllowDecimalOutOfRange = true
m.AllowTimestampOutOfRange = true
defer func() {
if err := m.PilosaClient().DeleteIndexByName(m.Index); err != nil {
t.Logf("deleting test index: %v", err)
}
}()
err := m.Run()
if err != nil {
t.Fatalf("running: %v", err)
}
client := m.PilosaClient()
schema, err := client.Schema()
if err != nil {
t.Fatalf("getting schema: %v", err)
}
index := schema.Index(m.Index)
resp, err := client.Query(index.Count(index.All()))
if err != nil {
t.Fatalf("querying: %v", err)
}
if cnt := resp.Result().Count(); cnt != 14 {
t.Fatalf("expected 14, got %+v", cnt)
}
for _, field := range []string{"ts", "age", "price"} {
pql := fmt.Sprintf("Extract(All(), Rows(%s))", field)
eResp, err := idktest.DoExtractQuery(pql, m.Index)
if err != nil {
t.Fatal("doing extract: ", err)
}
if eResp.Results[0].Columns == nil {
t.Fatal("no results: ", err)
}
for i, item := range eResp.Results[0].Columns {
check := checker[field].([]interface{})
switch exp := check[i].(type) {
case nil:
if item.Rows[0] != nil {
t.Errorf("expected nil, got %+v", item.Rows[0])
}
case string:
if item.Rows[0] != exp {
t.Errorf("expected %s, got %+v", exp, item.Rows[0])
}
case int:
vAsInt := int(item.Rows[0].(float64))
if vAsInt != exp {
t.Errorf("expected %d, got %+v", exp, item.Rows[0])
} else {
expAsFloat := float64(exp)
if item.Rows[0] != expAsFloat {
t.Errorf("expected %f, got %+v", expAsFloat, item.Rows[0])
}
}
case float64:
if item.Rows[0] != exp {
t.Errorf("expected %f, got %+v", exp, item.Rows[0])
}
default:
t.Errorf("unknown type: %T", exp)
}
}
}
}
// Test that out of range int values are ingested as nil when AllowIntOutOfRange is true.
func TestIntOpts(t *testing.T) {
file := `
id__ID,negneg__Int_-10_-5,negpos__Int_-10_10,pospos__Int_5_10,negzero__Int_-10_0,zeropos__Int_0_10,zerozero__Int_0_0
1,-20,-20,-20,-20,-20,-20
2,-10,-10,-10,-10,-10,-10
3,-5,-5,-5,-5,-5,-5
4,0,0,0,0,0,0
5,5,5,5,5,5,5
6,10,10,10,10,10,10
7,20,20,20,20,20,20
`[1:]
checker := make(map[string]interface{})
checker["negneg"] = []interface{}{nil, -10, -5, nil, nil, nil, nil}
checker["negzero"] = []interface{}{nil, -10, -5, 0, nil, nil, nil}
checker["negpos"] = []interface{}{nil, -10, -5, 0, 5, 10, nil}
checker["zeropos"] = []interface{}{nil, nil, nil, 0, 5, 10, nil}
checker["zerozero"] = []interface{}{nil, nil, nil, 0, nil, nil, nil}
checker["pospos"] = []interface{}{nil, nil, nil, nil, 5, 10, nil}
batchSizes := []int{1, 3, 4, 10}
for _, bsize := range batchSizes {
t.Run(fmt.Sprintf("batchsize=%d", bsize), func(t *testing.T) {
m := newMainOORFactory(t, file, true, false, false)
m.BatchSize = bsize
defer func() {
if err := m.PilosaClient().DeleteIndexByName(m.Index); err != nil {
t.Logf("deleting test index: %v", err)
}
}()
err := m.Run()
if err != nil {
t.Fatalf("running: %v", err)
}
for _, field := range []string{"negneg", "negzero", "negpos", "zeropos", "pospos"} {
pql := fmt.Sprintf("Extract(All(), Rows(%s))", field)
eResp, err := idktest.DoExtractQuery(pql, m.Index)
if err != nil {
t.Fatal("doing extract: ", err)
}
if eResp.Results[0].Columns == nil {
t.Fatal("no results: ", err)
}
for i, item := range eResp.Results[0].Columns {
check := checker[field].([]interface{})
switch exp := check[i].(type) {
case nil:
if item.Rows[0] != nil {
t.Errorf("expected nil, got %+v", item.Rows[0])
}
case string:
if item.Rows[0] != exp {
t.Errorf("expected %s, got %+v", exp, item.Rows[0])
}
case int:
vAsInt := int(item.Rows[0].(float64))
if vAsInt != exp {
t.Errorf("expected %d, got %+v", exp, item.Rows[0])
} else {
expAsFloat := float64(exp)
if item.Rows[0] != expAsFloat {
t.Errorf("expected %f, got %+v", expAsFloat, item.Rows[0])
}
}
case float64:
if item.Rows[0] != exp {
t.Errorf("expected %f, got %+v", exp, item.Rows[0])
}
default:
t.Errorf("unknown type: %T", exp)
}
}
}
})
}
}
// Test that out of range timestamp values are ingested as nil when AllowTimestampOutOfRange is true.
func TestTimestampOOR(t *testing.T) {
file := `
id__ID,ts1__Timestamp_s_2006-01-02 15:04:05.999,ts2__Timestamp_s_2006-01-02T15:04:05Z07:00_2261-12-31T15:04:05Z_h,ts3__Timestamp_s_2006-01-02T15:04:05Z07:00_1679-12-31T15:04:05Z_h,ts4__Timestamp_s_2006-01-02 15:04:05.999
0,1833-01-03 08:00:00.000,2431,-19960,2009-11-24 17:31:44.000
1,1833-11-24 17:31:44.000,2433,-19955,1800-11-24 17:31:44.000
2,1833-11-25 17:31:44.000,9999,0,2011-11-25 17:31:44.000
3,2106-02-06 06:28:16.000,0,-99999,2008-02-06 06:28:16.000
4,2106-02-07 06:28:16.000,9999,-99999,1800-02-07 06:28:16.000
5,2106-02-08 06:28:16.000,99999999999999999999999,-9999999999999999999999999,2012-02-07 06:28:16.000
`[1:]
checker := make(map[string]interface{})
// should import nil if timestamp val is out of range
checker["ts1"] = []interface{}{nil, "1833-11-24T17:31:44Z", "1833-11-25T17:31:44Z", "2106-02-06T06:28:16Z", "2106-02-07T06:28:16Z", nil}
// should import nil if custom epoch + value overflows
checker["ts2"] = []interface{}{"2262-04-11T22:04:05Z", nil, nil, "2261-12-31T15:04:05Z", nil, nil}
checker["ts3"] = []interface{}{nil, "1677-09-21T04:04:05Z", "1679-12-31T15:04:05Z", nil, nil, nil}
checker["ts4"] = []interface{}{"2009-11-24T17:31:44Z", nil, "2011-11-25T17:31:44Z", "2008-02-06T06:28:16Z", nil, "2012-02-07T06:28:16Z"}
batchSizes := []int{3, 1, 4, 10}
for _, bsize := range batchSizes {
t.Run(fmt.Sprintf("batchsize=%d", bsize), func(t *testing.T) {
m := newMainOORFactory(t, file, false, false, true)
m.BatchSize = bsize
defer func() {
if err := m.PilosaClient().DeleteIndexByName(m.Index); err != nil {
t.Logf("deleting test index: %v", err)
}
}()
err := m.Run()
if err != nil {
t.Fatalf("running: %v", err)
}
for _, field := range []string{"ts1", "ts2", "ts3", "ts4"} {
pql := fmt.Sprintf("Extract(All(), Rows(%s))", field)
eResp, err := idktest.DoExtractQuery(pql, m.Index)
if err != nil {
t.Fatal("doing extract: ", err)
}
if eResp.Results[0].Columns == nil {
t.Fatal("no results: ", err)
}
for i, item := range eResp.Results[0].Columns {
check := checker[field].([]interface{})
switch exp := check[i].(type) {
case nil:
if item.Rows[0] != nil {
t.Errorf("expected nil, got %+v", item.Rows[0])
}
case string:
if item.Rows[0] != exp {
t.Errorf("expected %s, got %+v", exp, item.Rows[0])
}
default:
t.Errorf("unknown type: %T", exp)
}
}
}
})
}
}
// Tests various conditions that should halt ingest
func TestFailureConditions(t *testing.T) {
type testCase struct {
name string
csv string
fail bool
intOutOfRange bool
decimalOutOfRange bool
timestampOutOfRange bool
}
testCases := []testCase{
{name: "too small", csv: `id__ID,ts1__Timestamp_s_2006-01-02T15:04:05Z07:00_1600-12-31T15:04:05Z_h
0,0
`, fail: true, intOutOfRange: true, timestampOutOfRange: true, decimalOutOfRange: true},
{name: "just right", csv: `id__ID,ts1__Timestamp_s_2006-01-02T15:04:05Z07:00_2200-12-31T15:04:05Z_h
0,0
`, fail: false, intOutOfRange: true, timestampOutOfRange: true, decimalOutOfRange: true},
{name: "too big", csv: `id__ID,ts1__Timestamp_s_2006-01-02T15:04:05Z07:00_2262-12-31T15:04:05Z_h
0,0
`, fail: true, intOutOfRange: true, timestampOutOfRange: true, decimalOutOfRange: true},
{name: "intOutOfRange not allowed", csv: `id__ID,pospos__Int_5_10
0,4
`, fail: true, intOutOfRange: false, timestampOutOfRange: true, decimalOutOfRange: true},
{name: "intOutOfRange not allowed-2", csv: `id__ID,pospos__Int_5_10
0,11
`, fail: true, intOutOfRange: false, timestampOutOfRange: true, decimalOutOfRange: true},
{name: "timestampOutOfRange not allowed", csv: `id__ID,ts1__Timestamp_s_2006-01-02 15:04:05.999
0,1833-01-03 08:00:00.000
`, fail: true, intOutOfRange: true, timestampOutOfRange: false, decimalOutOfRange: true},
{name: "timestampOutOfRange not allowed-2", csv: `id__ID,ts2__Timestamp_s_2006-01-02T15:04:05Z07:00_2261-12-31T15:04:05Z_h
0,2433
`, fail: true, intOutOfRange: true, timestampOutOfRange: false, decimalOutOfRange: true},
{name: "decimalOutOfRange not allowed", csv: `id__ID,price__Decimal_2
0,994492233720368547758.0892233720368547758
`, fail: true, intOutOfRange: true, timestampOutOfRange: true, decimalOutOfRange: false},
{name: "intOverflow", csv: `id__ID,pospos__Int
0,89273948723984729387492387492987
`, fail: true, intOutOfRange: false, timestampOutOfRange: false, decimalOutOfRange: false},
{name: "int string overflow", csv: `id__ID,pospos__Int
0,"89273948723984729387492387492987"
`, fail: true, intOutOfRange: false, timestampOutOfRange: false, decimalOutOfRange: false}}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
m := newMainOORFactory(t, test.csv, test.intOutOfRange, test.decimalOutOfRange, test.timestampOutOfRange)
m.BatchSize = 1
defer func() {
if err := m.PilosaClient().DeleteIndexByName(m.Index); err != nil {
t.Logf("deleting test index: %v", err)
}
}()
err := m.Run()
if test.fail {
if err == nil {
t.Fatal("expected error, got nil")
}
} else {
if err != nil {
t.Fatalf("running: %v", err)
}
}
})
}
}
func newMainOORFactory(t *testing.T, file string, allowIntOOR bool, allowDecOOR bool, allowTSOOR bool) *Main {
name := writeTempFile(t, file)
m := NewMain()
m.AutoGenerate = true
configureTestFlags(m)
m.Files = []string{name}
rand.Seed(time.Now().UnixNano())
m.Index = fmt.Sprintf("oortest%d", rand.Intn(100000))
m.AllowIntOutOfRange = allowIntOOR
m.AllowDecimalOutOfRange = allowDecOOR
m.AllowTimestampOutOfRange = allowTSOOR
return m
}

187
idk/csv/source.go Normal file
View file

@ -0,0 +1,187 @@
package csv
import (
"context"
"encoding/csv"
"io"
"net/http"
"os"
"reflect"
"strings"
"sync"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
"github.com/pkg/errors"
)
type Source struct {
Files chan string
Header []string
IgnoreHeader bool
JustDoIt bool
Log logger.Logger
schemaLock sync.Mutex
schema []idk.Field
records chan Record
once *sync.Once
expectHeader bool
}
func (s *Source) Record() (idk.Record, error) {
s.once.Do(func() { go s.run() })
rec, ok := <-s.records
if !ok {
return nil, io.EOF
}
return rec, rec.err
}
type Record struct {
data []interface{}
err error
}
func (r Record) Data() []interface{} {
return r.data
}
func (r Record) Commit(ctx context.Context) error { return nil } // TODO do
func (s *Source) Schema() []idk.Field {
s.schemaLock.Lock()
defer s.schemaLock.Unlock()
return s.schema
}
func (s *Source) run() {
defer close(s.records)
s.expectHeader = len(s.Header) == 0
if !s.expectHeader {
var err error
s.schemaLock.Lock()
s.schema, err = s.processHeader(s.Header)
s.schemaLock.Unlock()
if err != nil {
s.records <- Record{err: errors.Wrapf(err, "processing given header: %+v", s.Header)}
return
}
}
for filename := range s.Files {
s.processFile(filename)
}
}
func (s *Source) processHeader(header []string) (schema []idk.Field, err error) {
schema = make([]idk.Field, len(header))
for i, val := range header {
schema[i], err = idk.HeaderToField(val, s.Log)
if err != nil && s.JustDoIt {
schema[i] = idk.StringField{NameVal: strings.ToLower(val)}
} else if err != nil {
return nil, err
}
}
return schema, nil
}
func (s *Source) processFile(name string) {
s.Log.Printf("processFile: %s", name)
f, err := openFileOrURL(name)
if err != nil {
s.records <- Record{err: errors.Wrapf(err, "opening %s", name)}
return
}
defer f.Close()
reader := csv.NewReader(f)
reader.ReuseRecord = true
reader.FieldsPerRecord = 0
var nextErr error
if s.expectHeader || s.IgnoreHeader {
header, err := reader.Read()
if err != nil {
s.records <- Record{err: errors.Wrapf(err, "reading header from '%s'", name)}
return
}
if s.expectHeader {
newschema, err := s.processHeader(header)
if err != nil {
s.Log.Printf("processHeader error: %v\n", err)
s.records <- Record{err: errors.Wrapf(err, "processing header from '%s': %+v", name, header)}
return
}
if !reflect.DeepEqual(newschema, s.schema) {
s.schema = newschema
nextErr = idk.ErrSchemaChange
}
}
}
// weird hack to reuse memory rather than allocating eery
// time. Would probably be better to get rid of the channel and
// just the next CSV row on demand.
recs := [2]Record{
{data: make([]interface{}, len(s.schema)), err: nextErr},
{data: make([]interface{}, len(s.schema))},
}
i := -1
extraColumnsCount := 0
row, err := reader.Read()
for ; err == nil; row, err = reader.Read() {
i += 1
for j, val := range row {
if len(s.schema) <= j {
if extraColumnsCount == 0 {
s.Log.Warnf("'%s': ignoring additional column(s) not included in the header specification", name)
}
extraColumnsCount++
break
}
recs[i%2].data[j] = val
}
s.records <- recs[i%2]
recs[i%2].err = nil
}
if extraColumnsCount > 0 {
s.Log.Printf("Processing '%s': %d rows have more columns than header specification", name, extraColumnsCount)
}
if err != io.EOF {
s.Log.Printf("ERROR Processing '%s': '%v'. Skipping rest of file.", name, err)
}
}
func NewSource() *Source {
return &Source{
records: make(chan Record, 0), // nolint: gosimple // do not change buffer size!
}
}
func openFileOrURL(name string) (io.ReadCloser, error) {
var content io.ReadCloser
if strings.HasPrefix(name, "http") {
resp, err := http.Get(name)
if err != nil {
return nil, errors.Wrap(err, "getting via http")
}
if resp.StatusCode > 299 {
return nil, errors.Errorf("got status %d via http.Get", resp.StatusCode)
}
content = resp.Body
} else {
f, err := os.Open(name)
if err != nil {
return nil, errors.Wrap(err, "opening file")
}
content = f
}
return content, nil
}
func (s *Source) Close() error {
return nil
}

39
idk/datagen/README.md Normal file
View file

@ -0,0 +1,39 @@
# Datagen Tool
## Help Usage
```sh
Usage of datagen:
-c, --concurrency int Number of concurrent sources and indexing routines to launch. (default 1)
--dry-run Dry run - just flag parsing.
-e, --end-at uint ID at which to stop generating records.
--kafka.batch-size int Number of records to generate before sending them to Kafka all at once. Generally, larger means better throughput and more memory usage. (default 1000)
--kafka.hosts strings Comma separated list of host:port pairs for Kafka. (default [])
--kafka.registry-url string Location of Confluent Schema Registry. Must start with 'https://' if you want to use TLS.
--kafka.subject string Kafka schema subject.
--kafka.topic string Kafka topic to post to.
--pilosa.batch-size int Number of records to read before indexing all of them at once. Generally, larger means better throughput and more memory usage. 1,048,576 might be a good number.
--pilosa.cache-length uint Number of batches of ID mappings to cache. (default 64)
--pilosa.hosts strings Comma separated list of host:port pairs for Pilosa. (default [])
--pilosa.index string Name of Pilosa index.
--seed int Seed to use for any random number generation.
-s, --source string Source generator type. Running datagen with no arguments will list the available source types.
-b, --start-from uint ID at which to start generating records.
-t, --target string Destination for the generated data: [kafka, pilosa]. (default "pilosa")
--track-progress Periodically print status updates on how many records have been sourced.
```
## Example Usage
The following command will create 100 records in Pilosa index (starting at ID 0 and ending at ID 99)
in the `equipment` index using the `equipment` data generator.
```sh
datagen --source=equipment --pilosa.index=equipment --end-at=99
```
## Adding New Sources
TODO: redo README (or delete?)
If you're looking to add a new Source to datagen, the best thing to do is use the special "custom" datagen source (`datagen --source=custom --custom-config=somefile.yaml`) and write a `somefile.yaml` which describes the data you want to generate. An example can be found in `datagen/testdata/custom.yaml`, and there are some more in the molecula/technical-validation repo.

View file

@ -0,0 +1,319 @@
package datagen
import (
"io"
"sort"
"strconv"
"time"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/datagen/gen"
"github.com/molecula/featurebase/v3/pql"
)
// Ensure AllFieldTypes implements interface.
var _ Sourcer = (*AllFieldTypes)(nil)
// AllFieldTypes implements Sourcer, and returns a data
// set containing one of every field type.
type AllFieldTypes struct{}
// NewAllFieldTypes returns a new instance of AllFieldTypes.
func NewAllFieldTypes(cfg SourceGeneratorConfig) Sourcer {
return &AllFieldTypes{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (a *AllFieldTypes) Source(cfg SourceConfig) idk.Source {
epoch := time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC)
layout := "2006-01-02"
src := &AllFieldTypesSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
epoch: epoch,
layout: layout,
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.BoolField{NameVal: "bool"}, // 1
idk.DateIntField{
NameVal: "dateint",
Epoch: epoch,
Unit: idk.Day,
Layout: layout,
}, // 2
idk.DecimalField{NameVal: "decimal", Scale: 3}, // 3
idk.IDArrayField{NameVal: "idarray"}, // 4
idk.IDField{NameVal: "idfield"}, // 5
//idk.IgnoreField{}, // TODO: maybe an "ignore" property instead?
idk.IntField{
NameVal: "int",
Min: intptr(-100000),
Max: intptr(200000),
}, // 6 // TODO: foreignIndex
idk.RecordTimeField{
NameVal: "recordtime",
Layout: layout,
}, // 7
idk.SignedIntBoolKeyField{NameVal: "signedintboolkey"}, // 8
idk.StringArrayField{NameVal: "stringarray"}, // 9
idk.StringField{NameVal: "string"}, // 10
// Include `mutex` fields.
idk.IDField{NameVal: "idfieldmutex", Mutex: true}, // 11
idk.StringField{NameVal: "stringmutex", Mutex: true}, // 12
// Include `quantum` fields.
idk.IDArrayField{NameVal: "idarrayquantum", Quantum: "YMD", TTL: "0s"}, // 13
idk.IDField{NameVal: "idfieldquantum", Quantum: "YMD", TTL: "0s"}, // 14
idk.StringArrayField{NameVal: "stringarrayquantum", Quantum: "YMD", TTL: "0s"}, // 15
idk.StringField{NameVal: "stringquantum", Quantum: "YMD", TTL: "0s"}, // 16
idk.TimestampField{NameVal: "timestamp", Unit: idk.Second}, // 17
},
}
src.record = make([]interface{}, len(src.schema))
src.g = gen.New(gen.OptGenSeed(cfg.seed))
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (a *AllFieldTypes) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (a *AllFieldTypes) DefaultEndAt() uint64 {
return 100
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (a *AllFieldTypes) Info() string {
return "Generates data for all field types."
}
// Ensure ExampleSource implements interface.
var _ idk.Source = (*AllFieldTypesSource)(nil)
// AllFieldTypesSource is an instance of a source generated
// by the Sourcer implementation AllFieldTypes.
type AllFieldTypesSource struct {
g *gen.Gen
cur uint64
endAt uint64
epoch time.Time
layout string
schema []idk.Field
record record
}
// Record implements idk.Source.
func (a *AllFieldTypesSource) Record() (idk.Record, error) {
if a.cur >= a.endAt {
return nil, io.EOF
}
// Increment the ID.
a.record[0] = a.cur
// 1 - Bool
switch a.cur % 7 {
case 0:
a.record[1] = nil
case 1:
a.record[1] = true
case 2:
a.record[1] = false
case 3:
a.record[1] = "true"
case 4:
a.record[1] = "false"
case 5:
a.record[1] = 1
case 6:
a.record[1] = 0
}
// 2 - DateIntField
switch a.cur % 2 {
case 0:
a.record[2] = nil
case 1:
a.record[2] = a.epoch.AddDate(0, 0, int(a.cur)).Format(a.layout)
}
// 3 - Decimal
switch a.cur % 2 {
case 0:
a.record[3] = nil
case 1:
a.record[3] = pql.NewDecimal(int64(1003*a.cur), 2)
}
// 4 - IDArray
switch a.cur % 8 {
case 7:
a.record[4] = nil
default:
// Generate a random set field.
set := a.g.Set(100, 1000, 10)
vals := make([]uint64, 0, len(set))
for v := range set {
vals = append(vals, v)
}
a.record[4] = vals
}
// 5 - ID (pilosa rowID field, not primary key)
switch a.cur % 6 {
case 5:
a.record[5] = nil
default:
a.record[5] = a.g.R.Int63n(100000)
}
// x - IgnoreField
// 6 - Int
switch a.cur % 9 {
case 8:
a.record[6] = nil
default:
a.record[6] = a.g.R.Int63n(300000) - 100000
}
// 7 - RecordTime
switch a.cur % 7 {
case 8:
a.record[7] = nil
default:
a.record[7] = a.epoch.AddDate(5, 0, int(a.cur)).Format(a.layout)
}
// 8 - SignedIntBoolKey
switch a.cur % 10 {
case 4:
a.record[8] = nil
default:
a.record[8] = int64(2*(a.cur%2)-1) * a.g.R.Int63n(10000)
}
// 9 - StringArray
switch a.cur % 7 {
case 5:
a.record[9] = nil
default:
// Generate a random set field.
set := a.g.Set(100, 1000, 10)
vals := make([]string, 0, len(set))
for v := range set {
vals = append(vals, "v"+strconv.FormatUint(v, 10))
}
sort.Strings(vals)
a.record[9] = vals
}
// 10 - String
switch a.cur % 8 {
case 5:
a.record[10] = nil
default:
rint := a.g.R.Int63n(2000)
a.record[10] = "s" + strconv.FormatUint(uint64(rint), 10)
}
// 11 - ID, mutex
switch a.cur % 16 {
case 6:
a.record[11] = nil
default:
a.record[11] = a.g.R.Int63n(100000)
}
// 12 - String, mutex
switch a.cur % 17 {
case 5:
a.record[12] = nil
default:
rint := a.g.R.Int63n(2000)
a.record[12] = "sm" + strconv.FormatUint(uint64(rint), 10)
}
// 13 - IDArray
switch a.cur % 9 {
case 2:
a.record[13] = nil
default:
// Generate a random set field.
set := a.g.Set(100, 1000, 10)
vals := make([]uint64, 0, len(set))
for v := range set {
vals = append(vals, v)
}
a.record[13] = vals
}
// 14 - ID, quantum
switch a.cur % 14 {
case 7:
a.record[14] = nil
default:
a.record[14] = a.g.R.Int63n(100000)
}
// 15 - StringArray, quantum
switch a.cur % 18 {
case 7:
a.record[15] = nil
default:
// Generate a random set field.
set := a.g.Set(100, 1000, 10)
vals := make([]string, 0, len(set))
for v := range set {
vals = append(vals, "sa"+strconv.FormatUint(v, 10))
}
sort.Strings(vals)
a.record[15] = vals
}
// 16 - String, quantum
switch a.cur % 17 {
case 5:
a.record[16] = nil
default:
rint := a.g.R.Int63n(2000)
a.record[16] = "sq" + strconv.FormatUint(uint64(rint), 10)
}
// 17 - Timestamp
switch a.cur % 15 {
case 8:
a.record[17] = nil
default:
rint := a.g.R.Int63n(604800) // seconds in a week
a.record[17] = 1605041948 + rint // offset by sometime in 2021
}
a.cur++
return a.record, nil
}
// Schema implements idk.Source.
func (a *AllFieldTypesSource) Schema() []idk.Field {
return a.schema
}
func (a *AllFieldTypesSource) Close() error {
return nil
}

311
idk/datagen/bank.go Normal file
View file

@ -0,0 +1,311 @@
package datagen
import (
"fmt"
"io"
"math/rand"
"strconv"
"strings"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/datagen/gen"
)
// Ensure Bank implements interface.
var _ Sourcer = (*Bank)(nil)
// Bank implements Sourcer.
type Bank struct {
schema []idk.Field
}
// NewBank returns a new instance of a Bank data generator.
func NewBank(cfg SourceGeneratorConfig) Sourcer {
return &Bank{
schema: []idk.Field{
idk.StringField{NameVal: "aba", Mutex: true},
idk.StringField{NameVal: "db", Mutex: true},
idk.IntField{NameVal: "user_id"},
idk.StringField{NameVal: "custom_audiences"},
// TODO generalize
idk.DecimalField{NameVal: floatNames[0]},
idk.BoolField{NameVal: boolNames[0]},
idk.BoolField{NameVal: boolNames[1]},
idk.StringField{NameVal: stringNames[0]},
idk.IntField{NameVal: intNames[0]},
},
}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (b *Bank) Source(cfg SourceConfig) idk.Source {
src := &BankSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
schema: b.schema,
rand: rand.New(rand.NewSource(cfg.seed)),
}
src.g = gen.New(gen.OptGenSeed(cfg.seed))
src.record = make([]interface{}, len(src.schema))
// TODO alloc?
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (b *Bank) PrimaryKeyFields() []string {
return []string{"aba", "db", "user_id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (b *Bank) DefaultEndAt() uint64 {
return 20000
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (b *Bank) Info() string {
return "Generates user data representative of a multi-site banking application"
}
// Ensure BankSource implements interface.
var _ idk.Source = (*BankSource)(nil)
// Ensure BankSource implements interface.
//var _ idk.KafkaSource = (*BankSource)(nil) // TODO
// BankSource is a data generator which generates
// data for all Pilosa field types.
type BankSource struct {
g *gen.Gen
cur, endAt uint64
schema []idk.Field
record record
rand *rand.Rand
}
var (
// kafka cruft a.k.a. kruftka
floatMeta = `"type": ["null", {"type": "float", "scale": 3}], "default": null`
boolMeta = `"type": ["null", "boolean"], "default": null`
stringMeta = `"type": ["null", "string"], "default": null, "mutex": true`
intMeta = `"type": ["null", "int"], "default": null`
floatNames = []string{
"pfm_category_total_current_balance__personal_loan",
}
boolNames = []string{
"pfm_boolean__personal_loan",
"pfm_boolean__mortgage",
}
stringNames = []string{
"survey_5dfe1a89_29fa_4505_ac00_4cd055b758ab",
}
intNames = []string{
"product_recency__consumer_loan",
}
floatNullChance = 0.1
boolNullChance = 0.1
stringNullChance = 0.1
intNullChance = 0.1
// TODO these are effectively consts right now
floatCount = 1
boolCount = 2
stringCount = 1
intCount = 1
)
// Record implements idk.Source.
func (b *BankSource) Record() (idk.Record, error) {
if b.cur >= b.endAt {
return nil, io.EOF
}
// Increment the ID.
//b.record[0] = b.cur
b.cur++
b.record[0] = b.ABA()
b.record[1] = b.Db()
b.record[2] = b.UserID()
b.record[3] = b.CustomAudiences()
// TODO generalize
if floatNullChance < b.rand.Float64() {
b.record[4] = b.rand.Float64()
}
// TODO: handle null chance
if boolNullChance < b.rand.Float64() {
b.record[5] = b.rand.Intn(2) == 1
}
// TODO: handle null chance
if boolNullChance < b.rand.Float64() {
b.record[6] = b.rand.Intn(2) == 1
}
// TODO: handle null chance
if stringNullChance < b.rand.Float64() {
b.record[7] = text(b.rand, 1, 6, true, true, true, false)
}
// TODO: handle null chance
if intNullChance < b.rand.Float64() {
b.record[8] = int64(b.rand.Intn(1000000))
}
// TODO: handle null chance
return b.record, nil
}
// KafkaRecord might implement idk.KafkaSource
// TODO: going to want to generate the kafka record from the idk record
// automatic handling would be nice, but allowing per-source is probably important,
// e.g. to handle special cases for individual kafka environments
func (b *BankSource) KafkaRecord() string {
fields := []string{
`"aba": "` + b.ABA() + `"`,
`"db": "` + b.Db() + `"`,
`"user_id": ` + strconv.FormatInt(int64(b.UserID()), 10),
`"custom_audiences": {"string": "` + b.CustomAudiences() + `"}`,
}
for n := 0; n < floatCount; n++ {
if floatNullChance < b.rand.Float64() {
floatStr := strconv.FormatFloat(b.rand.Float64(), 'f', 3, 64)
fields = append(fields, `"float_`+floatNames[n]+`": {"float": `+floatStr+`}`)
}
}
for n := 0; n < boolCount; n++ {
if boolNullChance < b.rand.Float64() {
boolStr := strconv.FormatBool(b.rand.Intn(2) == 1)
fields = append(fields, `"bool_`+boolNames[n]+`": {"boolean": `+boolStr+`}`)
}
}
for n := 0; n < stringCount; n++ {
if stringNullChance < b.rand.Float64() {
stringStr := text(b.rand, 1, 6, true, true, true, false)
fields = append(fields, `"string_`+stringNames[n]+`": {"string": "`+stringStr+`"}`)
}
}
for n := 0; n < intCount; n++ {
if intNullChance < b.rand.Float64() {
intStr := strconv.FormatInt(int64(b.rand.Intn(1000000)), 10)
fields = append(fields, `"int_`+intNames[n]+`": {"int": `+intStr+`}`)
}
}
return `{` + strings.Join(fields, ", ") + `}`
}
// ABA returns a random 9 numeric digit string with about 27000 possible values.
func (b *BankSource) ABA() string {
num := b.rand.Intn(27000) + 22213
num2 := num/10 - 1213
numstr := strconv.Itoa(num)
num2str := strconv.Itoa(num2)
numstrbytes := append([]byte(numstr), num2str[3], numstr[0], numstr[1], numstr[2])
return string(numstrbytes)
}
// Db returns a db
func (b *BankSource) Db() string {
return text(b.rand, 1, 6, true, true, true, false)
}
// UserID returns a user ID
func (b *BankSource) UserID() int {
return b.rand.Intn(10000000) // 10 mil
}
// CustomAudiences returns a fake Custom Audience string
func (b *BankSource) CustomAudiences() string {
return text(b.rand, 1, 6, true, true, true, false)
}
// TODO: move to gen.go
var lowerLetters = []rune("abcdefghijklmnopqrstuvwxyz")
var upperLetters = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
var numeric = []rune("0123456789")
var specialChars = []rune(`!'@#$%^&*()_+-=[]{};:",./?`)
func text(rand *rand.Rand, atLeast, atMost int, allowLower, allowUpper, allowNumeric, allowSpecial bool) string {
allowedChars := []rune{}
if allowLower {
allowedChars = append(allowedChars, lowerLetters...)
}
if allowUpper {
allowedChars = append(allowedChars, upperLetters...)
}
if allowNumeric {
allowedChars = append(allowedChars, numeric...)
}
if allowSpecial {
allowedChars = append(allowedChars, specialChars...)
}
result := []rune{}
nTimes := rand.Intn(atMost-atLeast+1) + atLeast
for i := 0; i < nTimes; i++ {
result = append(result, allowedChars[rand.Intn(len(allowedChars))])
}
return string(result)
}
// Schema implements idk.Source
func (b *BankSource) Schema() []idk.Field {
return b.schema
}
// KafkaSchema implements idk.KafkaSource
func (b *BankSource) KafkaSchema() string {
meta := `"type": "record",
"name": "bank_user_data",
"namespace": "bank.user.data",
"doc": "Per-user bank data"`
fields := []string{
`{"name": "aba", "type": "string"}`,
`{"name": "db", "type": "string"}`,
`{"name": "user_id", "type": "int"}`,
`{"name": "custom_audiences", "type": ["null", "string"], "default": null}`,
}
// TODO redmove loops if Schema() is not generalized
for n := 0; n < floatCount; n++ {
fields = append(fields, `{"name": "float_`+floatNames[n]+`", `+floatMeta+`}`)
}
for n := 0; n < boolCount; n++ {
fields = append(fields, `{"name": "bool_`+boolNames[n]+`", `+boolMeta+`}`)
}
for n := 0; n < stringCount; n++ {
fields = append(fields, `{"name": "string_`+stringNames[n]+`", `+stringMeta+`}`)
}
for n := 0; n < intCount; n++ {
fields = append(fields, `{"name": "int_`+intNames[n]+`", `+intMeta+`}`)
}
schema := fmt.Sprintf(`{%s, "fields": [%s]}`, meta, strings.Join(fields, ", "))
return schema
}
func (b *BankSource) Close() error {
return nil
}

128
idk/datagen/claim.go Normal file
View file

@ -0,0 +1,128 @@
package datagen
import (
"io"
"math/rand"
"time"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
)
// Ensure Claim implements interface.
var _ Sourcer = (*Claim)(nil)
// Claim implements Sourcer.
type Claim struct{}
// NewClaim returns a new instance of Claim.
func NewClaim(cfg SourceGeneratorConfig) Sourcer {
return &Claim{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (c *Claim) Source(cfg SourceConfig) idk.Source {
min := int64(0)
src := &ClaimSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(19)),
schema: []idk.Field{
idk.IDField{NameVal: "id"},
idk.StringField{NameVal: "service_type"},
idk.IntField{NameVal: "date"},
idk.IntField{NameVal: "item_id", Min: &min},
idk.StringArrayField{NameVal: "part"},
},
}
src.record = make([]interface{}, len(src.schema))
src.partZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(parts)-1))
src.record[4] = make([]string, 4)
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (c *Claim) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (c *Claim) DefaultEndAt() uint64 {
return 50000000
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (c *Claim) Info() string {
return "TODO"
}
// Ensure ClaimSource implements interface.
var _ idk.Source = (*ClaimSource)(nil)
type ClaimSource struct {
Log logger.Logger
cur, endAt uint64
rand *rand.Rand
partZipf *rand.Zipf
schema []idk.Field
record record
}
var parts = []string{"cpu", "ram", "motherboard", "cooler", "case", "speaker", "microphone", "nic", "wlan", "screen", "keyboard", "disk", "ssd", "optical", "fan", "powersupp", "part0", "part1", "part2", "part3", "part4", "part5", "part6", "part7", "part8", "part9", "part10", "part11", "part12", "part13", "part14", "part15", "part16", "part17", "part18", "part19", "part20", "part21", "part22", "part23", "part24", "part25", "part26", "part27", "part28", "part29", "part30", "part31", "part32", "part33", "part34", "part35", "part36", "part37", "part38", "part39", "part40", "part41", "part42", "part43", "part44", "part45", "part46", "part47", "part48", "part49", "part50", "part51", "part52", "part53", "part54", "part55", "part56", "part57", "part58", "part59", "part60", "part61", "part62", "part63", "part64", "part65", "part66", "part67", "part68", "part69", "part70", "part71", "part72", "part73", "part74", "part75", "part76", "part77", "part78", "part79", "part80", "part81", "part82", "part83", "part84"}
var claimTypes = []string{"warranty", "damage", "spill", "intentional", "accidental", "collateral", "priority", "replacement", "reimbursement"}
func (s *ClaimSource) Record() (idk.Record, error) {
if s.cur >= s.endAt {
return nil, io.EOF
}
s.record[0] = s.cur
s.record[1] = claimTypes[s.rand.Intn(len(claimTypes))]
s.record[2] = int64(time.Duration(s.rand.Intn(int(currentDur))) / (time.Hour * 24)) // day
s.record[3] = s.rand.Intn(1500000000)
s.record[4] = s.record[4].([]string)[:randNumParts(s.rand)]
for i := range s.record[4].([]string) {
s.record[4].([]string)[i] = parts[s.partZipf.Uint64()]
}
s.cur++
return s.record, nil
}
func (s *ClaimSource) Schema() []idk.Field {
return s.schema
}
func (s *ClaimSource) Seed(seed int64) {
s.rand.Seed(seed)
}
var _ Seedable = (*ClaimSource)(nil)
func randNumParts(r *rand.Rand) int {
n := r.Intn(100)
if n < 80 {
return 1
}
if n < 90 {
return 2
}
if n < 95 {
return 3
}
return 4
}
func (s *ClaimSource) Close() error {
return nil
}

607
idk/datagen/cmd.go Normal file
View file

@ -0,0 +1,607 @@
package datagen
import (
"context"
"fmt"
"math"
"math/rand"
"os"
"sort"
"strconv"
"strings"
"sync"
"github.com/glycerine/vprint"
pilosaclient "github.com/molecula/featurebase/v3/client"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
"github.com/pkg/errors"
"github.com/molecula/featurebase/v3/idk/common"
"github.com/molecula/featurebase/v3/idk/kafka"
)
const (
TargetFeaturebase = "featurebase"
TargetKafka = "kafka"
TargetKafkaStatic = "kafkastatic"
)
// Main is the top-level datagen struct. It represents datagen-specific
// configuration parameters, as well as sub-level parameteres specific
// to each target.
type Main struct {
idkMain *idk.Main
KafkaPut *kafka.PutSource `flag:"-"`
srcs []idk.Source
srcLock sync.Mutex
cfg SourceGeneratorConfig
Source string `short:"s" flag:"source" help:"Source generator type. Running datagen with no arguments will list the available source types."`
Target string `short:"t" flag:"target" help:"Destination for the generated data: [kafka, featurebase]."`
Concurrency int `short:"c" flag:"concurrency" help:"Number of concurrent sources and indexing routines to launch."`
StartFrom uint64 `short:"b" help:"ID at which to start generating records."`
EndAt uint64 `short:"e" help:"ID at which to stop generating records."`
Seed int64 `short:"" help:"Seed to use for any random number generation."`
TrackProgress bool `short:"" help:"Periodically print status updates on how many records have been sourced."`
UseIngestAPI bool `help:"Experimental: use new HTTP/JSON ingest API instead of low-level import API. Probably slow, does not support packed bools."`
UseShardTransactionalEndpoint bool `flag:"use-shard-transactional-endpoint" help:"Use experimental transactional endpoint"`
CustomConfig string `short:"" help:"File from which to pull configuration for 'custom' source."`
// Used strictly for configuration of the targets.
Pilosa PilosaConfig
Kafka KafkaConfig
DryRun bool `help:"Dry run - just flag parsing."`
AuthToken string `flag:"auth-token" help:"Authentication token for FeatureBase"`
Verbose bool `flag:"verbose" help:"Enable extended logging for debug"`
Datadog bool `flag:"datadog" help:"Enable datadog profiling"`
}
// PilosaConfig is meant to represent the scoped (pilosa.*) configuration options
// to be used when target = pilosa. These are really just a sub-set of idk.Main,
// containing only those arguments that really apply to datagen.
type PilosaConfig struct {
Hosts []string `short:"" help:"Comma separated list of host:port pairs for FeatureBase."`
Index string `short:"" help:"Name of FeatureBase index."`
BatchSize int `short:"" help:"Number of records to read before indexing all of them at once. Generally, larger means better throughput and more memory usage. 1,048,576 might be a good number."`
CacheLength uint64 `help:"Number of batches of ID mappings to cache."`
}
// KafkaConfig is meant to represent the scoped (pilosa.*) configuration options
// to be used when target = kafka. These are really just a sub-set of kafka.PutSource,
// containing only those arguments that really apply to datagen.
type KafkaConfig struct {
idk.ConfluentCommand
Topic string `short:"" help:"Kafka topic to post to."`
Subject string `short:"" help:"Kafka schema subject."`
BatchSize int `short:"" help:"Number of records to generate before sending them to Kafka all at once. Generally, larger means better throughput and more memory usage."`
ReplicationFactor int `short:"" help:"set replication factor for kafka cluster"`
NumPartitions int `short:"" help:"set partition for kafka cluster"`
}
// NewMain returns a new instance of Main.
func NewMain() *Main {
return &Main{
idkMain: &idk.Main{},
KafkaPut: &kafka.PutSource{},
Target: TargetFeaturebase,
Concurrency: 1,
Pilosa: PilosaConfig{
CacheLength: 64,
},
Kafka: KafkaConfig{
BatchSize: 1000,
ReplicationFactor: 1,
NumPartitions: 1,
},
}
}
// Sourcer is an interface describing a type which can generate sources,
// which in this case are `idk.Source`. It contains a few additional
// methods which are used to configure the source.
type Sourcer interface {
Source(SourceConfig) idk.Source
PrimaryKeyFields() []string
DefaultEndAt() uint64
Info() string
}
// SourceConfig is the configuration required by a Sourcer when creating
// a new Source.
type SourceConfig struct {
startFrom uint64
endAt uint64
total uint64
seed int64
}
// sourcerTypes is a map of all available custom Sourcer
// implementations.
var sourcerTypes = map[string]func(SourceGeneratorConfig) Sourcer{
"all-field-types": NewAllFieldTypes,
"bank": NewBank,
"claim": NewClaim,
"customer_segmentation": NewPerson,
"customer_segmentation_linkedin": NewLinkedInPerson,
"customer": NewCustomer,
"custom": NewCustom,
"dell": NewDell,
"dwarranty": NewDWarranty,
"example": NewExample,
"hughes": NewHughes,
"equipment": NewEquipment,
"network_ts": NewNetwork,
"power_ts": NewTimeseries,
"site": NewSite,
"transactions_ts": NewTransaction,
"item": NewItem,
"kitchensink": NewKitchenSink,
"kitchensink_keyed": NewKitchenSinkKeyed,
"merck": NewMerck,
"paloalto": NewPaloAlto,
"power_scenario_1": NewPower1,
"power_scenario_1_2": NewPower1_2,
"sizing": NewSizing,
"stringpk": NewStringPK,
"texas_health": NewTexasHealth,
"transactions_scenario_1": NewTransaction1,
"warranty": NewWarranty,
}
var sourcerTypeGroups = map[string][]string{
"iot": {
"equipment",
"network_ts",
"power_ts",
"site",
"transactions_ts",
},
}
func (m *Main) info() string {
// Sort the sourcerTypes map by key.
keys := make([]string, 0, len(sourcerTypes))
for k := range sourcerTypes {
keys = append(keys, k)
}
sort.Strings(keys)
ret := "\n------------------------------------------\n"
ret += "A source must be specified with --source\n"
ret += "The following source types are supported:\n\n"
alreadyPrinted := make(map[string]struct{})
for group, keys := range sourcerTypeGroups {
ret += fmt.Sprintf(" %s\n", group)
for _, k := range keys {
info, _ := typeInfo(k)
ret += fmt.Sprintf(" %-21s: %s\n", k, info)
alreadyPrinted[k] = struct{}{}
}
}
for _, k := range keys {
if _, ok := alreadyPrinted[k]; ok {
continue
}
info, _ := typeInfo(k)
ret += fmt.Sprintf(" %-25s: %s\n", k, info)
}
ret += "\n"
return ret
}
func typeInfo(key string) (string, error) {
srcrFn, ok := sourcerTypes[key]
if !ok {
return "", errors.Errorf("invalid key: %s", key)
}
src := srcrFn(SourceGeneratorConfig{})
return src.Info(), nil
}
// Preload configures the Sources based on the concurrency
// and start/end range.
func (m *Main) Preload() error {
if m.Source == "" {
return errors.New(m.info())
}
cfg := SourceGeneratorConfig{
StartFrom: m.StartFrom,
EndAt: m.EndAt,
Concurrency: m.Concurrency,
Seed: m.Seed,
CustomConfig: m.CustomConfig,
}
pks, srcs, err := m.makeSources(m.Source, cfg)
if err != nil {
return errors.Wrap(err, "getting sources")
}
m.idkMain = idk.NewMain()
// TODO: this is a little hacky because of the complexity around
// PrimaryKeyFields and IDField, and the fact that those are
// currently being set before having a schema.
// See: https://github.com/molecula/featurebase/v3/idk/issues/147
switch {
case len(pks) == 1 && pks[0] == "id":
m.idkMain.IDField = "id"
case len(pks) > 0:
m.idkMain.IDField = ""
m.idkMain.PrimaryKeyFields = pks
default:
m.idkMain.AutoGenerate = true
m.idkMain.ExternalGenerate = true
}
switch m.Target {
case TargetKafka, TargetKafkaStatic:
m.KafkaPut, _ = kafka.NewPutSource()
m.KafkaPut.Concurrency = m.Concurrency
m.KafkaPut.NewSource = m.newSource
m.KafkaPut.TrackProgress = m.TrackProgress
m.KafkaPut.BatchSize = m.Kafka.BatchSize
m.KafkaPut.ConfluentCommand = m.Kafka.ConfluentCommand
m.KafkaPut.ConfigMap, err = common.SetupConfluent(&m.Kafka.ConfluentCommand)
m.KafkaPut.Target = m.Target
if err != nil {
return errors.Wrap(err, "setup confluent")
}
if m.Kafka.Topic != "" {
m.KafkaPut.Topic = m.Kafka.Topic
}
if m.Kafka.Subject != "" {
m.KafkaPut.Subject = m.Kafka.Subject
}
if m.Verbose {
m.KafkaPut.Log = logger.NewVerboseLogger(os.Stderr)
}
m.KafkaPut.ReplicationFactor = m.Kafka.ReplicationFactor
m.KafkaPut.NumPartitions = m.Kafka.NumPartitions
m.KafkaPut.FBPrimaryKeyFields = m.idkMain.PrimaryKeyFields
m.KafkaPut.FBIDField = m.idkMain.IDField
m.KafkaPut.FBIndexName = m.Pilosa.Index
default:
m.idkMain.Namespace = "ingester_datagen"
m.idkMain.Concurrency = m.Concurrency
m.idkMain.CacheLength = m.Pilosa.CacheLength
m.idkMain.NewSource = m.newSource
m.idkMain.TrackProgress = m.TrackProgress
m.idkMain.UseIngestAPI = m.UseIngestAPI
m.idkMain.AuthToken = m.AuthToken
m.idkMain.UseShardTransactionalEndpoint = m.UseShardTransactionalEndpoint
if len(m.Pilosa.Hosts) > 0 {
m.idkMain.PilosaHosts = m.Pilosa.Hosts
}
m.idkMain.Index = m.Pilosa.Index
if m.Pilosa.BatchSize > 0 {
m.idkMain.BatchSize = m.Pilosa.BatchSize
}
}
m.srcs = srcs
return nil
}
// Sources returns the list of sources. This is typically
// called by tests, after the generator has created the sources
// during Preload().
func (m *Main) Sources() []idk.Source {
return m.srcs
}
// Run generates data for the specified target.
func (m *Main) Run() error {
switch m.Target {
case TargetKafka, TargetKafkaStatic:
return m.KafkaPut.Run()
default:
return m.idkMain.Run()
}
}
func (m *Main) PrintPlan() {
cfg := m.cfg
startFrom := cfg.StartFrom
endAt := cfg.EndAt - 1 // adjust for the implementation detail `endAt++` in generator.Sources
concurrency := cfg.Concurrency
RecordCount := endAt - startFrom
switch m.Target {
case TargetKafka:
Topic := m.Kafka.Topic
NumPartitions := m.Kafka.NumPartitions
ReplicationFactor := m.Kafka.ReplicationFactor
Subject := m.Kafka.Subject
Hosts := m.KafkaPut.KafkaBootstrapServers
RegURL := m.KafkaPut.SchemaRegistryURL
if m.Kafka.Topic == "" {
Topic = m.KafkaPut.Topic
}
if m.Kafka.Subject == "" {
Subject = m.KafkaPut.Subject
}
fmt.Printf(`Datagen config:
hosts: %s
start id: %s
end id: %s
total generated: %s
concurrency: %d
registry URL: %s
topic: %s
subject: %s
num partitions: %d
replication factor: %d
`,
strings.Join(Hosts, ", "),
AddThousandSep(startFrom),
AddThousandSep(endAt),
AddThousandSep(RecordCount),
concurrency,
RegURL,
Topic,
Subject,
NumPartitions,
ReplicationFactor,
)
case TargetKafkaStatic:
Topic := m.Kafka.Topic
NumPartitions := m.Kafka.NumPartitions
ReplicationFactor := m.Kafka.ReplicationFactor
Subject := m.Kafka.Subject
Hosts := m.KafkaPut.KafkaBootstrapServers
if m.Kafka.Topic == "" {
Topic = m.KafkaPut.Topic
}
if m.Kafka.Subject == "" {
Subject = m.KafkaPut.Subject
}
fmt.Printf(`Datagen config:
hosts: %s
start id: %s
end id: %s
total generated: %s
concurrency: %d
topic: %s
subject: %s
num partitions: %d
replication factor: %d
`,
strings.Join(Hosts, ", "),
AddThousandSep(startFrom),
AddThousandSep(endAt),
AddThousandSep(RecordCount),
concurrency,
Topic,
Subject,
NumPartitions,
ReplicationFactor,
)
default:
Hosts := m.Pilosa.Hosts
BatchSize := m.Pilosa.BatchSize
if len(m.Pilosa.Hosts) <= 0 {
Hosts = m.idkMain.PilosaHosts
}
if m.Pilosa.BatchSize <= 0 {
BatchSize = m.idkMain.BatchSize
}
BatchCount := uint64(math.Ceil(float64(RecordCount) / float64(BatchSize)))
if m.Pilosa.Index == "" {
m.Pilosa.Index = "(not specified)"
}
fmt.Printf(`Datagen config:
hosts: %s
index: %s
start id: %s
end id: %s
total generated: %s
concurrency: %d
batch size: %s
total batches: %s
`,
strings.Join(Hosts, ", "),
m.Pilosa.Index,
AddThousandSep(startFrom),
AddThousandSep(endAt),
AddThousandSep(RecordCount),
concurrency,
AddThousandSep(uint64(BatchSize)),
AddThousandSep(BatchCount),
)
fmt.Println("Schema:")
for _, field := range m.srcs[0].Schema() {
fmt.Printf("%T %+[1]v\n", field)
}
}
}
func AddThousandSep(num uint64) string {
in := strconv.FormatUint(num, 10)
out := ""
for i := 1; i <= len(in); i++ {
out = string(in[len(in)-i]) + out
if i != 0 && i != len(in) && i%3 == 0 {
out = "," + out
}
}
return out
}
// newSource pops the next idk.Source from srcs.
func (m *Main) newSource() (source idk.Source, err error) {
m.srcLock.Lock()
defer m.srcLock.Unlock()
if len(m.srcs) < 1 {
return nil, errors.New("requested too many sources")
}
source = m.srcs[0]
m.srcs = m.srcs[1:]
return source, nil
}
// PilosaClient is a helper for tests. The datagen command
// used to embed idk.Main, so datagen tests could call
// Main.PilosaClient(), but idk.Main is no longer
// directly embedded.
func (m *Main) PilosaClient() *pilosaclient.Client {
if m.idkMain == nil {
return nil
}
return m.idkMain.PilosaClient()
}
// NoStats is a helper for tests.
func (m *Main) NoStats() {
if m.idkMain != nil {
m.idkMain.Stats = ""
}
}
// Sources implements the SourceGenerator interface.
func (m *Main) makeSources(key string, cfg SourceGeneratorConfig) ([]string, []idk.Source, error) {
srcrFn, ok := sourcerTypes[key]
if !ok {
return nil, nil, errors.Errorf("invalid key: %s", key)
}
srcr := srcrFn(cfg)
// Get the primary key fields for the source.
pks := srcr.PrimaryKeyFields()
startFrom, endAt := cfg.StartFrom, cfg.EndAt
if endAt == 0 {
if dflt := srcr.DefaultEndAt(); dflt > 0 {
endAt = dflt
} else {
endAt = startFrom + 99 // Default to 100 records.
}
}
if startFrom > endAt {
return nil, nil, errors.Errorf("invalid start/end: %d/%d", startFrom, endAt)
}
endAt++ // endAt is used as `a.cur >= a.endAt`, so increment it to get the expected, inclusive, behavior.
// Calculate the concurrency, total, and start/end boundaries
// based on the configuration values.
m.cfg = SourceGeneratorConfig{
StartFrom: startFrom,
EndAt: endAt,
Concurrency: cfg.Concurrency,
Seed: cfg.Seed,
CustomConfig: cfg.CustomConfig,
}
concurrency, startEnds, total := startEnds(m.cfg)
seedGen := rand.New(rand.NewSource(cfg.Seed))
srcs := make([]idk.Source, concurrency)
// Distribute the total range of records over a number of
// Sources equal to the concurrency.
for i := 0; i < concurrency; i++ {
srcCfg := SourceConfig{
startFrom: startEnds[i].start,
endAt: startEnds[i].end,
total: total,
seed: cfg.Seed,
}
vprint.VV("source config for i: %d, %#v", i, srcCfg)
src := srcr.Source(srcCfg)
// TODO: pull seed from config?
if s, ok := src.(Seedable); ok {
s.Seed(seedGen.Int63())
}
srcs[i] = src
}
return pks, srcs, nil
}
// startEnds returns the start/end boundaries for each source
// run needed based on cfg.StartFrom, cfg.EndAt, and cfg.Concurrency.
// It also returns the calculated total, and the final concurrency
// (in case it had to be adjusted).
// returns (concurrency, startEnds, total)
func startEnds(cfg SourceGeneratorConfig) (int, []startEnd, uint64) {
var startFrom uint64 = cfg.StartFrom
var endAt uint64 = cfg.EndAt
var concurrency int = cfg.Concurrency
if concurrency == 0 {
return 0, []startEnd{}, 0
}
total := endAt - startFrom
if uint64(concurrency) > total {
concurrency = int(total)
}
var current uint64
interval := total / uint64(concurrency)
if startFrom > current {
current = startFrom
}
leftover := total - (interval * uint64(concurrency))
ses := make([]startEnd, concurrency)
// Distribute the total range of records over a number of
// Sources equal to the concurrency.
for i := 0; i < concurrency; i++ {
var start uint64 = current
var end uint64
current += interval
// Include one of the leftovers in each iteration until there
// are no more.
if uint64(i) < leftover {
current++
}
// start is inclusive, end is exclusive
end = current
ses[i] = startEnd{
start: start,
end: end,
}
}
return concurrency, ses, total
}
type record []interface{}
func (r record) Commit(ctx context.Context) error { return nil }
func (r record) Data() []interface{} {
return r
}
type startEnd struct {
start uint64
end uint64
}

34
idk/datagen/common.go Normal file
View file

@ -0,0 +1,34 @@
package datagen
import (
"github.com/molecula/featurebase/v3/idk"
)
// SourceGenerator is an interface for anything which can generate
// data by providing one or more idk.Source. It also contains
// a method for getting information about the supported sources.
// Info() - return information about the generator along with
// a list of supported types
// Sources() - provided a string key and configuration, returns
// a list of primary key fields and a list of sources.
type SourceGenerator interface {
Info() string
Sources(string, SourceGeneratorConfig) ([]string, []idk.Source, error)
Config() SourceGeneratorConfig
}
// SourceGeneratorConfig provides configuration values used by
// implementations of the SourceGenerator interface.
type SourceGeneratorConfig struct {
StartFrom uint64
EndAt uint64
Concurrency int
Seed int64
CustomConfig string
}
// Seedable is an interface representing anything for which
// a seed can be provided.
type Seedable interface {
Seed(int64)
}

949
idk/datagen/custom.go Normal file
View file

@ -0,0 +1,949 @@
package datagen
import (
"bufio"
"encoding/csv"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"os"
"sort"
"strconv"
"time"
"github.com/molecula/featurebase/v3/idk"
"github.com/pkg/errors"
"sigs.k8s.io/yaml"
)
// Ensure Custom implements Sourcer interface.
var _ Sourcer = (*Custom)(nil)
// Custom implements Sourcer
type Custom struct {
err error
CustomConfig *CustomConfig
IDKAndGenFields *IDKAndGenFields
}
// NewCustom returns a new instance of Custom.
func NewCustom(cfg SourceGeneratorConfig) Sourcer {
conf := &CustomConfig{}
if bytes, err := ioutil.ReadFile(cfg.CustomConfig); err != nil {
return &Custom{err: errors.Wrap(err, "reading custom config file")}
} else if err = yaml.Unmarshal(bytes, conf); err != nil {
return &Custom{err: errors.Wrap(err, "unmarshaling custom config file")}
} else if err := conf.PostUnmarshal(); err != nil {
return &Custom{err: errors.Wrap(err, "post unmarshal")}
} else if ig, err := conf.GetIDKFields(); err != nil {
return &Custom{err: errors.Wrap(err, "getting IDK fields from custom config file")}
} else {
return &Custom{
CustomConfig: conf,
IDKAndGenFields: ig,
}
}
}
// CustomConfig represents the JSON/yaml configuration for the "custom" datagen source.
type CustomConfig struct {
// Fields describe the data to be generated.
Fields []*GenField `json:"fields"`
// IDKParams configure how the generated data should be treated by
// the IDK (and probably ultimately ingested into FeatureBase).
IDKParams IDKParams `json:"idk_params"`
}
// GenField describes one field of the data to be generated. Many of
// the struct fields are only used for certain values of the 'Type'
// field. See the commented custom.yaml example as a reference.
//
// GenField says nothing about what should be done with the generated
// data, or how it might map to FeatureBase fields.
//
// NOTE: If you add fields here, make sure you add them to the custom
// UnmarshalJSON method below.
type GenField struct {
Name string `json:"name"`
Type string `json:"type"`
Distribution string `json:"distribution"`
Min int64 `json:"min"`
Max int64 `json:"max"`
MinFloat float64 `json:"min_float"`
MaxFloat float64 `json:"max_float"`
Repeat bool `json:"repeat"`
Step int64 `json:"step"`
S float64 `json:"s"`
V float64 `json:"v"`
SourceFile string `json:"source_file"`
GeneratorType string `json:"generator_type"`
Cardinality uint64 `json:"cardinality"`
Charset string `json:"charset"`
MinLen uint64 `json:"min_len"`
MaxLen uint64 `json:"max_len"`
MinNum uint64 `json:"min_num"`
MaxNum uint64 `json:"max_num"`
MinStepDuration string `json:"min_step_duration"`
MaxStepDuration string `json:"max_step_duration"`
minStepDuration time.Duration
maxStepDuration time.Duration
MinDate time.Time `json:"min_date"`
MaxDate time.Time `json:"max_date"`
TimeFormat TimeFormat `json:"time_format"`
TimeUnit idk.Unit `json:"time_unit"`
NullChance float64 `json:"null_chance"` // between 0 and 1.0. Fraction of the time that we get a null value for this field
}
// PostUnmarshal does post-processing of the CustomConfig object after
// the basic JSON/yaml unmarshal, like converting time strings to
// durations. There's probably an elegant way to do this during the
// Unmarshal, but I couldn't figure it out.
func (c *CustomConfig) PostUnmarshal() error {
for i, g := range c.Fields {
if g.MinStepDuration != "" {
dur, err := time.ParseDuration(g.MinStepDuration)
if err != nil {
return errors.Wrap(err, "parsing step_duration_min")
}
c.Fields[i].minStepDuration = dur
}
if g.MaxStepDuration != "" {
dur, err := time.ParseDuration(g.MaxStepDuration)
if err != nil {
return errors.Wrap(err, "parsing step_duration_max")
}
c.Fields[i].maxStepDuration = dur
}
}
return nil
}
// DefaultIDKField returns default IDK configuration for a given
// GenField. This is convenient for the common case of generated data
// wanting to be ingested into FeatureBase so that the user doesn't
// have to specify explicit "idk_params" configuration for every
// field.
func (g *GenField) DefaultIDKField() (idk.Field, error) {
switch g.Type {
case "uint":
return idk.IDField{NameVal: g.Name}, nil
case "int":
var min int64 = g.Min
var max int64 = g.Max
return idk.IntField{NameVal: g.Name, Min: &min, Max: &max}, nil
case "string":
return idk.StringField{NameVal: g.Name}, nil
case "string-set":
return idk.StringArrayField{NameVal: g.Name}, nil
case "uint-set":
return idk.IDArrayField{NameVal: g.Name}, nil
case "timestamp":
return idk.TimestampField{NameVal: g.Name}, nil
case "float":
return idk.DecimalField{NameVal: g.Name, Scale: 2}, nil
default:
return nil, errors.Errorf("no default IDK field for type: '%s'", g.Type)
}
}
// IDKParams describes how data from CustomConfig.Fields should map to IDK fields.
type IDKParams struct {
Fields map[string][]IngestField `json:"fields"`
PrimaryKeyConfig PrimaryKeyConfig `json:"primary_key_config"`
}
// IngestField is a json/yaml configuration for an IDK Field. See the
// ToIDKField method for how this configuration maps to each possible
// IDK Field.
type IngestField struct {
Type string `json:"type"`
Name string `json:"name"`
Layout string `json:"layout"`
Epoch time.Time `json:"epoch"`
Unit string `json:"unit"`
Keyed bool `json:"keyed"`
Mutex bool `json:"mutex"`
TimeQuantum string `json:"time_quantum"`
Min *int64 `json:"min"`
Max *int64 `json:"max"`
Scale int64 `json:"scale"`
ForeignIndex string `json:"foreign_index"`
Granularity string `json:"granularity"`
TTL string `json:"ttl"`
}
// ToIDKField converts the general IngestField to a specific IDK Field.
func (f IngestField) ToIDKField(g *GenField) (idk.Field, error) {
name := g.Name
destName := g.Name
if f.Name != "" {
destName = f.Name
}
switch f.Type {
case "ID":
return idk.IDField{NameVal: name, DestNameVal: destName, Quantum: f.TimeQuantum, Mutex: f.Mutex, TTL: f.TTL}, nil
case "IDArray":
return idk.IDArrayField{NameVal: name, DestNameVal: destName, Quantum: f.TimeQuantum, TTL: f.TTL}, nil
case "Int":
return idk.IntField{NameVal: name, DestNameVal: destName, Min: f.Min, Max: f.Max, ForeignIndex: f.ForeignIndex}, nil
case "RecordTime":
return idk.RecordTimeField{NameVal: name, DestNameVal: destName, Layout: f.Layout, Epoch: f.Epoch, Unit: idk.Unit(f.Unit)}, nil
case "String":
return idk.StringField{NameVal: name, DestNameVal: destName, Mutex: f.Mutex, Quantum: f.TimeQuantum, TTL: f.TTL}, nil
case "StringArray":
return idk.StringArrayField{NameVal: name, DestNameVal: destName, Quantum: f.TimeQuantum, TTL: f.TTL}, nil
case "Timestamp":
return idk.TimestampField{NameVal: name, DestNameVal: destName, Layout: f.Layout, Epoch: f.Epoch, Unit: idk.Unit(f.Unit), Granularity: f.Granularity}, nil
case "Decimal":
return idk.DecimalField{NameVal: name, DestNameVal: destName, Scale: f.Scale}, nil
default:
return nil, errors.Errorf("unsupported idk field type: '%s' for field named: '%s'", f.Type, f.Name)
}
}
// PrimaryKeyConfig specifies what should be interpreted as the record
// identifier in Featurebase.
type PrimaryKeyConfig struct {
Field string `json:"field"`
}
// IDKAndGenFields keeps the IDK schema and generated data in
// sync. schema and genFields will always be the same length, and the
// idk.Fields in schema will always be non-nil. Items in genFields may
// be nil which indicates that the corresponding idk.Field in schema
// will use data generated by the last non-nil genField.
// e.g.
//
// schema: IDField, RecordTimeField, TimestampField
// genFields: Gen{"id"}, Gen{"timestamp"}, nil
//
// The TimestampField will get values from Gen{"timestamp"} since its
// corresponding genField is nil.
type IDKAndGenFields struct {
schema []idk.Field
genFields []*GenField
}
// Append adds to IDKAndGenFields and ensures that schema and genFields stay in sync.
func (ig *IDKAndGenFields) Append(f idk.Field, g *GenField) {
ig.schema = append(ig.schema, f)
ig.genFields = append(ig.genFields, g)
}
// GetIDKFields figures out what IDK Fields should be created based on
// the generated data fields and the IDK configuration parameters. If
// multiple IDK fields are generated for a single genField, the
// corresponding entries beyond the first in IDKAndGenFields.genFields
// will be nil. We'll only create one generator and use the value it
// generates for all those fields in the generated record.
func (cc *CustomConfig) GetIDKFields() (*IDKAndGenFields, error) {
ig := &IDKAndGenFields{}
for _, genField := range cc.Fields {
idkFieldConfig, ok := cc.IDKParams.Fields[genField.Name]
if !ok {
idkField, err := genField.DefaultIDKField()
if err != nil {
return ig, errors.Wrapf(err, "getting default IDK field for %+v", genField)
}
ig.Append(idkField, genField)
continue
}
for i, ingestField := range idkFieldConfig {
idkField, err := ingestField.ToIDKField(genField)
if err != nil {
return ig, errors.Wrap(err, "getting configured IDK field")
}
if i == 0 {
ig.Append(idkField, genField)
} else {
ig.Append(idkField, nil)
}
}
}
return ig, nil
}
// Source returns a configured Custom idk.Source. It does not
// currently have explicit support for concurrency or paritioned data
// generation as some of the other datagen sources do.
func (c *Custom) Source(cfg SourceConfig) idk.Source {
if c.err != nil {
log.Fatal(c.err) // TODO add error return arg to Sourcer interface
}
r := rand.New(rand.NewSource(cfg.seed))
generators := make([]FieldGenerator, len(c.IDKAndGenFields.genFields))
for i, g := range c.IDKAndGenFields.genFields {
var err error
if g == nil {
generators[i] = nil
continue
}
generators[i], err = g.Generator(r)
log.Printf("Generator for %s, %+v\n", g.Name, generators[i])
if err != nil {
log.Fatal(errors.Wrap(err, "getting generators"))
}
}
log.Printf("schema: %+v", c.IDKAndGenFields.schema)
recordsToGenerate := uint64(0)
if cfg.endAt > 0 {
recordsToGenerate = cfg.endAt - cfg.startFrom
}
return &CustomSource{
conf: c.CustomConfig,
schema: c.IDKAndGenFields.schema,
generators: generators,
record: make([]interface{}, len(generators)),
recordsToGenerate: recordsToGenerate,
}
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (c *Custom) PrimaryKeyFields() []string {
if c.err != nil {
log.Fatal(errors.Wrap(c.err, "can't get primary key fields"))
}
return []string{c.CustomConfig.IDKParams.PrimaryKeyConfig.Field}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface. Not used for Custom.
func (_ *Custom) DefaultEndAt() uint64 {
return 0
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (_ *Custom) Info() string {
return "Generates data from a yaml definition file."
}
// Ensure CustomSource implements interface.
var _ idk.Source = (*CustomSource)(nil)
// CustomSource is an instance of a source generated
// by the Sourcer implementation Custom.
type CustomSource struct {
conf *CustomConfig
generators []FieldGenerator
schema []idk.Field
record record
recordsToGenerate uint64
recordCounter uint64
}
// Schema returns the IDK fields which were determined from the data
// generation and ingest configuration parameters in the custom yaml
// file.
func (cs *CustomSource) Schema() []idk.Field {
return cs.schema
}
// Record uses the configured generators to emit a custom record. If
// any of the generators are nil, it uses the value from the last
// non-nil generator.
func (cs *CustomSource) Record() (idk.Record, error) {
// track last generated value
var last interface{}
for i, gen := range cs.generators {
// this indicates that the same value is being used for multiple IDK fields.
if gen == nil {
cs.record[i] = last
continue
}
var err error
cs.record[i], err = gen.Generate(cs.record[i])
if err == io.EOF {
return nil, nil
}
if err != nil {
return nil, errors.Wrapf(err, "generating for %+v", cs.schema[i])
}
last = cs.record[i]
}
if cs.recordsToGenerate > 0 && cs.recordCounter >= cs.recordsToGenerate {
//break when number records produced
return nil, io.EOF
} else {
cs.recordCounter++
}
return cs.record, nil
}
func (c *CustomSource) Close() error {
return nil
}
// FieldGenerator is an interface for generating values for a particular field.
type FieldGenerator interface {
// Generate produces a value. It takes in the previous value as an
// optimization—some implementations may opt to reuse a slice, for
// example.
Generate(oldval interface{}) (interface{}, error)
}
type NullChanceGenerator struct {
R *rand.Rand
NullChance float64
G FieldGenerator
}
func (g *NullChanceGenerator) Generate(oldval interface{}) (interface{}, error) {
if g.R.Float64() < g.NullChance {
return nil, nil
}
return g.G.Generate(oldval)
}
// Generator creates a data generator based on the GenField.
func (g *GenField) Generator(r *rand.Rand) (fg FieldGenerator, err error) {
if g.NullChance < 0 || g.NullChance > 1.0 {
return nil, errors.Errorf("null_chance must be in the range [0.0, 1.0], but got: %f", g.NullChance)
}
defer func() {
if err != nil {
return
}
if g.NullChance == 0.0 {
return
}
// if NullChance > 0, we'll wrap the generator we're going to return in a NullChanceGenerator
fg = &NullChanceGenerator{
R: r,
NullChance: g.NullChance,
G: fg,
}
}()
switch g.Type {
case "uint":
if g.Distribution != "sequential" && g.Distribution != "" {
return nil, errors.Errorf("unsupported distribution %s for uint field", g.Distribution)
}
if g.Step < 1 {
return nil, errors.Errorf("step must be >=1 for uint, got: %d", g.Step)
}
return &SequentialUintFieldGenerator{
cur: uint64(g.Min),
Min: uint64(g.Min),
Max: uint64(g.Max),
Repeat: g.Repeat,
Step: uint64(g.Step),
}, nil
case "int":
return getIntGenerator(g, r)
case "string":
return getStringGenerator(g, r)
case "string-set":
sg, err := getStringGenerator(g, r)
if err != nil {
return nil, errors.Wrap(err, "getting string generator")
}
return NewStringSetGenerator(g, r, sg)
case "uint-set":
ig, err := getIntGenerator(g, r)
if err != nil {
return nil, errors.Wrap(err, "getting int generator")
}
return &UintSetGenerator{
G: ig,
R: r,
MinNum: g.MinNum,
MaxNum: g.MaxNum,
}, nil
case "timestamp":
return getTimestampGenerator(g, r)
case "float":
switch g.Distribution {
case "uniform", "":
return &UniformFloatGenerator{
R: r,
MinFloat: g.MinFloat,
MaxFloat: g.MaxFloat,
}, nil
default:
return nil, errors.Errorf("unsupported distribution: '%s'", g.Distribution)
}
default:
return nil, errors.Errorf("unsupported type '%s'", g.Type)
}
}
func getIntGenerator(g *GenField, r *rand.Rand) (FieldGenerator, error) {
if g.Max == 0 && g.Min == 0 {
g.Max = 1000 // default
}
if g.Max <= g.Min {
return nil, errors.Errorf("max must be > min for int field min: %d, max: %d", g.Min, g.Max)
}
switch g.Distribution {
case "uniform", "":
return &UniformIntFieldGenerator{
R: r,
Min: g.Min,
Max: g.Max,
}, nil
case "zipfian":
z := rand.NewZipf(r, g.S, g.V, uint64(g.Max-g.Min))
if z == nil {
return nil, errors.Errorf("err getting int zipf: s: %f, v: %f, imax: %d", g.S, g.V, uint64(g.Max-g.Min))
}
return &ZipfianIntFieldGenerator{
Z: z,
Min: g.Min,
}, nil
default:
return nil, errors.Errorf("unsupported distribution for int, got: %s", g.Distribution)
}
}
func getStringGenerator(g *GenField, r *rand.Rand) (FieldGenerator, error) {
if g.SourceFile != "" {
vals, err := readLines(g.SourceFile)
if err != nil {
return nil, errors.Wrapf(err, "reading source file for field '%s'", g.Name)
}
switch g.Distribution {
case "zipfian", "":
if g.S == 0.0 && g.V == 0.0 {
g.S, g.V = 1.1, 5.1 // sane defaults
}
z := rand.NewZipf(r, g.S, g.V, uint64(len(vals)-1))
if z == nil {
return nil, errors.Errorf("err getting string zipf: s: %f, v: %f, imax: %d", g.S, g.V, uint64(len(vals)-1))
}
return &ZipfianStringSourceFieldGenerator{
Source: vals,
Z: z,
}, nil
case "uniform":
return &UniformStringSourceFieldGenerator{
Source: vals,
R: r,
}, nil
case "fixed": // use probabilities from file
vals, probs, err := readLinesWithProbabilities(g.SourceFile)
if err != nil {
return nil, errors.Wrapf(err, "reading source file for field '%s'", g.Name)
}
return NewFixedProbabilitySourceFieldGenerator(vals, probs, r)
default:
return nil, errors.Errorf("unknown distribution: '%s'", g.Distribution)
}
} else if g.GeneratorType == "random-string" {
if g.MinLen > g.MaxLen || g.MaxLen == 0 {
return nil, errors.Errorf("invalid max and min lengths: min(<=max): %d, max(>0): %d", g.MinLen, g.MaxLen)
}
return &RandomStringGenerator{
R: r,
MinLen: g.MinLen,
MaxLen: g.MaxLen,
Charset: g.Charset,
}, nil
} else if g.Distribution == "shifting" {
return NewShiftingStringGenerator(g, r)
}
return nil, errors.Errorf("unsupported string config for field '%s'", g.Name)
}
func getTimestampGenerator(g *GenField, r *rand.Rand) (FieldGenerator, error) {
switch g.Distribution {
case "increasing":
if g.TimeFormat == "" {
g.TimeFormat = timestampFormat
}
if !(g.TimeFormat == unixFormat || g.TimeFormat == timestampFormat) {
return nil, errors.Errorf("'%s' is not a valid output format", g.TimeFormat)
}
return &IncreasingTimestampGenerator{
R: r,
MinDate: g.MinDate,
MaxDate: g.MaxDate,
MaxStepDuration: g.maxStepDuration,
MinStepDuration: g.minStepDuration,
Repeat: g.Repeat,
OutputFormat: g.TimeFormat,
Unit: g.TimeUnit,
}, nil
default:
return nil, errors.Errorf("unimplemented distribution: %s", g.Distribution)
}
}
// SequentialUintFieldGenerator generates unsigned integers
// sequentially from a minimum up to a maximum with a configurable
// step. It will start from the beginning when it reaches the max if
// repeat==true.
type SequentialUintFieldGenerator struct {
cur uint64
Min uint64
Max uint64
Repeat bool
Step uint64
}
func (g *SequentialUintFieldGenerator) Generate(_ interface{}) (interface{}, error) {
ret := g.cur
g.cur += g.Step
if g.cur > g.Max {
if g.Repeat {
g.cur = g.Min
} else {
return nil, io.EOF
}
}
return ret, nil
}
// UniformIntFieldGenerator generates random integers between Min and Max with a uniform distribution
type UniformIntFieldGenerator struct {
R *rand.Rand
Min int64
Max int64
}
func (g *UniformIntFieldGenerator) Generate(_ interface{}) (interface{}, error) {
return g.R.Int63n(g.Max+1-g.Min) + g.Min, nil
}
type ZipfianIntFieldGenerator struct {
Z *rand.Zipf
Min int64
}
func (g *ZipfianIntFieldGenerator) Generate(_ interface{}) (interface{}, error) {
return int64(g.Z.Uint64()) + g.Min, nil
}
type ZipfianStringSourceFieldGenerator struct {
Source []string
Z *rand.Zipf
}
func (g *ZipfianStringSourceFieldGenerator) Generate(_ interface{}) (interface{}, error) {
return g.Source[g.Z.Uint64()], nil
}
type UniformStringSourceFieldGenerator struct {
Source []string
R *rand.Rand
}
func (g *UniformStringSourceFieldGenerator) Generate(_ interface{}) (interface{}, error) {
return g.Source[g.R.Intn(len(g.Source))], nil
}
func readLines(filename string) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
out := make([]string, 0)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
out = append(out, scanner.Text())
}
return out, nil
}
func readLinesWithProbabilities(filename string) ([]string, []float64, error) {
file, err := os.Open(filename)
if err != nil {
return nil, nil, err
}
defer file.Close()
values := make([]string, 0)
probabilities := make([]float64, 0)
r := csv.NewReader(file)
r.FieldsPerRecord = -1
r.ReuseRecord = true
var record []string
for record, err = r.Read(); err == nil; record, err = r.Read() {
if len(record) == 0 {
continue
}
values = append(values, record[0])
if len(record) > 1 {
var p float64
p, err = strconv.ParseFloat(record[1], 64)
if err != nil {
return nil, nil, errors.Wrapf(err, "could not parse '%s' as a probability", record[1])
}
probabilities = append(probabilities, p)
} else {
probabilities = append(probabilities, -1.0)
}
}
if err == io.EOF {
err = nil
}
return values, probabilities, err
}
type RandomStringGenerator struct {
R *rand.Rand
MinLen uint64
MaxLen uint64
Charset string
}
func (g *RandomStringGenerator) Generate(prev interface{}) (interface{}, error) {
length := g.R.Int63n(int64(g.MaxLen-g.MinLen)+1) + int64(g.MinLen)
buf, ok := prev.([]byte)
if !ok {
buf = make([]byte, g.MaxLen)
}
buf = buf[:0]
for i := int64(0); i < length; i++ {
buf = append(buf, g.Charset[g.R.Intn(len(g.Charset))])
}
return buf, nil
}
type StringSetGenerator struct {
G FieldGenerator // for generating individual strings
R *rand.Rand
MinNum uint64
MaxNum uint64
}
func NewStringSetGenerator(g *GenField, r *rand.Rand, fg FieldGenerator) (*StringSetGenerator, error) {
return &StringSetGenerator{
G: fg,
R: r,
MinNum: g.MinNum,
MaxNum: g.MaxNum,
}, nil
}
func (g *StringSetGenerator) Generate(prev interface{}) (interface{}, error) {
sslice, ok := prev.([]string)
if !ok {
sslice = make([]string, g.MaxNum)
}
sslice = sslice[:0]
num := g.R.Int63n(int64(g.MaxNum-g.MinNum)+1) + int64(g.MinNum)
var val []byte
for i := int64(0); i < num; i++ {
vali, err := g.G.Generate(val)
if err != nil {
return nil, errors.Wrap(err, "generating string")
}
if val, ok := vali.([]byte); ok {
sslice = append(sslice, string(val))
} else {
sslice = append(sslice, vali.(string))
}
}
return sslice, nil
}
type UintSetGenerator struct {
G FieldGenerator
R *rand.Rand
MinNum uint64
MaxNum uint64
}
func (g *UintSetGenerator) Generate(_ interface{}) (interface{}, error) {
num := g.R.Intn(int(g.MaxNum-g.MinNum)+1) + int(g.MinNum)
// ok, so we allocate a new slice for each record here, gross, I
// know. Problem is that IDK ultimately passes these to
// client.Batch.Add which adds the slice directly to a thing. This
// works differently for the string-set case (with []string) which
// is kind of horrible, and we should really make Batch.Add very
// clear and consistent about its semantics. TODO Right now
// though, we have this function which allocates a new slice every
// time, and the string ones above reuse the previous slice, and I
// just wanted to make sure everyone knows that that's on purpose.
uslice := make([]uint64, num)
for i := 0; i < num; i++ {
val, err := g.G.Generate(nil)
if err != nil {
return nil, errors.Wrap(err, "generating uint")
}
uslice[i] = uint64((val.(int64)))
}
return uslice, nil
}
type IncreasingTimestampGenerator struct {
R *rand.Rand
prev time.Time
MinDate time.Time
MaxDate time.Time
MinStepDuration time.Duration
MaxStepDuration time.Duration
Repeat bool
OutputFormat TimeFormat // "time", "int"
Unit idk.Unit // "s", "ms", "us", "ns"
}
type TimeFormat string
const (
timestampFormat = "timestamp" // time.Time
unixFormat = "unix" // unix epoch number (int64)
)
func (g *IncreasingTimestampGenerator) Generate(_ interface{}) (interface{}, error) {
step := time.Duration(g.R.Int63n(int64(g.MaxStepDuration)-int64(g.MinStepDuration)+1)) + g.MinStepDuration
if g.prev.IsZero() {
g.prev = g.MinDate
}
next := g.prev.Add(step)
if next.After(g.MaxDate) {
if !g.Repeat {
return nil, io.EOF
}
next = g.MinDate.Add(step)
}
g.prev = next
var ret interface{} = next
if g.OutputFormat == unixFormat {
unitDuration, err := g.Unit.Duration()
if err != nil {
return nil, err
}
ret = next.UnixNano() / int64(unitDuration)
}
return ret, nil
}
type UniformFloatGenerator struct {
R *rand.Rand
MinFloat float64
MaxFloat float64
}
func (g *UniformFloatGenerator) Generate(_ interface{}) (interface{}, error) {
return g.R.Float64()*(g.MaxFloat-g.MinFloat) + g.MinFloat, nil
}
type FixedProbabilitySourceFieldGenerator struct {
r *rand.Rand
source []string
probabilities []float64
}
func NewFixedProbabilitySourceFieldGenerator(vals []string, probs []float64, r *rand.Rand) (*FixedProbabilitySourceFieldGenerator, error) {
total := float64(0)
numUnspecified := 0
for _, prob := range probs {
if prob < 0 {
numUnspecified++
continue
}
total += prob
}
if total > 1.0 {
return nil, errors.Errorf("sum of all probabilities is greater than 1: %f", total)
}
unspecProb := 0.0
if numUnspecified > 0 {
unspecProb = (1.0 - total) / float64(numUnspecified)
}
total = float64(0)
for i, prob := range probs {
if prob < 0 {
probs[i] = unspecProb
}
total += prob
probs[i] = total
}
return &FixedProbabilitySourceFieldGenerator{
r: r,
source: vals,
probabilities: probs,
}, nil
}
func (g *FixedProbabilitySourceFieldGenerator) Generate(_ interface{}) (interface{}, error) {
val := g.r.Float64()
idx := sort.SearchFloat64s(g.probabilities, val)
if idx == len(g.probabilities) {
return nil, nil
}
return g.source[idx], nil
}
type ShiftingStringGenerator struct {
shiftAmnt uint64
z *rand.Zipf
i uint64
step uint64
baseLen uint64
lenVariance uint64
}
func (g *ShiftingStringGenerator) Generate(_ interface{}) (interface{}, error) {
num := g.z.Uint64() + g.shiftAmnt
g.i++
if g.i%g.step == 0 {
g.shiftAmnt++
}
length := g.baseLen + num%(g.lenVariance+1)
return numToString(num, length), nil
}
func numToString(num uint64, length uint64) string {
return fmt.Sprintf("%0*X", length, num)
}
func NewShiftingStringGenerator(g *GenField, r *rand.Rand) (*ShiftingStringGenerator, error) {
if g.MinLen+g.MaxLen != 0 {
if g.MinLen == 0 {
g.MinLen = g.MaxLen
} else if g.MaxLen == 0 {
g.MaxLen = g.MinLen
}
}
if g.MaxLen < g.MinLen {
return nil, errors.Errorf("max_len(%d) must be greater than or equal to min_len(%d)", g.MaxLen, g.MinLen)
}
if g.Step < 0 {
return nil, errors.Errorf("'step' for shifting must be positive, but got: %d", g.Step)
}
if g.Step == 0 {
g.Step = 1 // default to 1 if no step is passed
}
if g.S == 0 {
g.S = 1.01
}
if g.V == 0 {
g.V = 200
}
z := rand.NewZipf(r, g.S, g.V, g.Cardinality-1)
return &ShiftingStringGenerator{
z: z,
step: uint64(g.Step),
baseLen: g.MinLen,
lenVariance: g.MaxLen - g.MinLen,
}, nil
}

309
idk/datagen/custom_test.go Normal file
View file

@ -0,0 +1,309 @@
package datagen
import (
"io/ioutil"
"math/rand"
"os"
"strings"
"testing"
"time"
"github.com/molecula/featurebase/v3/idk"
)
func TestGetIDKFields(t *testing.T) {
cc := &CustomConfig{
Fields: []*GenField{
{
Name: "a",
Type: "uint",
},
{
Name: "b",
Type: "int",
},
},
IDKParams: IDKParams{
Fields: map[string][]IngestField{
"a": {
{
Type: "ID",
Name: "a",
},
{
Type: "Int",
Name: "a2",
},
{
Type: "ID",
Name: "ID_ttl",
TimeQuantum: "YMD",
TTL: "101s",
},
{
Type: "IDArray",
Name: "IDArray_ttl",
TimeQuantum: "YMD",
TTL: "102s",
},
{
Type: "String",
Name: "String_ttl",
TimeQuantum: "YMD",
TTL: "103s",
},
{
Type: "StringArray",
Name: "StringArray_ttl",
TimeQuantum: "YMD",
TTL: "104s",
},
},
},
},
}
min := int64(0)
max := int64(0)
exp := &IDKAndGenFields{
schema: []idk.Field{
idk.IDField{
NameVal: "a",
DestNameVal: "a",
},
idk.IntField{
NameVal: "a",
DestNameVal: "a2",
},
idk.IDField{
NameVal: "a",
DestNameVal: "ID_ttl",
Quantum: "YMD",
TTL: "101s",
},
idk.IDArrayField{
NameVal: "a",
DestNameVal: "IDArray_ttl",
Quantum: "YMD",
TTL: "102s",
},
idk.StringField{
NameVal: "a",
DestNameVal: "String_ttl",
Quantum: "YMD",
TTL: "103s",
},
idk.StringArrayField{
NameVal: "a",
DestNameVal: "StringArray_ttl",
Quantum: "YMD",
TTL: "104s",
},
idk.IntField{
NameVal: "b",
DestNameVal: "", // no dest because we take the default path
Min: &min,
Max: &max,
},
},
genFields: []*GenField{
{
Name: "a",
Type: "uint",
},
nil,
nil,
nil,
nil,
nil,
{
Name: "b",
Type: "int",
},
},
}
igs, err := cc.GetIDKFields()
if err != nil {
t.Fatalf("getting IDK fields: %v", err)
}
if len(igs.schema) != len(exp.schema) ||
len(igs.genFields) != len(exp.genFields) ||
len(igs.schema) != len(igs.genFields) {
t.Fatalf("mismatched lengths: %+v", igs)
}
for i := 0; i < len(igs.schema); i++ {
gotSchema, expSchema := igs.schema[i], exp.schema[i]
gotGenField, expGenField := igs.genFields[i], exp.genFields[i]
if !idk.FieldsEqual(gotSchema, expSchema) {
t.Fatalf("mismatched schemas at %d: got:\n%+v\nexp:\n%+v", i, gotSchema, expSchema)
}
if gotGenField == nil && expGenField == nil {
// great
} else if *gotGenField != *expGenField {
t.Fatalf("mismatched genFields at %d: got:\n%+v\nexp:\n%+v", i, gotGenField, expGenField)
}
}
}
func TestNewCustomUnmarshalling(t *testing.T) {
tmp, err := ioutil.TempFile("", "")
if err != nil {
t.Fatalf("creating temp file: %v", err)
}
defer os.Remove(tmp.Name())
_, err = tmp.WriteString(`
fields:
- name: "myfield"
type: "timestamp"
min_step_duration: "1s"
min_date: 2013-10-16T22:34:43Z
time_format: "unix"
time_unit: "ms"
`)
if err != nil {
t.Fatalf("writing: %v", err)
}
cc := NewCustom(SourceGeneratorConfig{CustomConfig: tmp.Name()}).(*Custom)
if cc.err != nil {
t.Fatalf("creating Custom: %v", cc.err)
}
if cc.CustomConfig.Fields[0].minStepDuration != time.Second {
t.Fatalf("wrong duration: %+v", cc.CustomConfig.Fields[0])
}
if cc.CustomConfig.Fields[0].MinDate.Hour() != 22 {
t.Fatalf("wrong time: %s", cc.CustomConfig.Fields[0].MinDate)
}
if cc.CustomConfig.Fields[0].TimeUnit != idk.Millisecond {
t.Fatalf("wrong time unit: %s", cc.CustomConfig.Fields[0].TimeUnit)
}
if cc.CustomConfig.Fields[0].TimeFormat != unixFormat {
t.Fatalf("wrong time format: %s", cc.CustomConfig.Fields[0].TimeFormat)
}
}
func TestIncreasingTimestampGenerator(t *testing.T) {
cases := []struct {
name string
unit idk.Unit
min int64
max int64
}{
{
name: "millis",
unit: idk.Millisecond,
max: 1136073600000 + 2001,
min: 1136073600000,
},
{
name: "days",
unit: idk.Day,
max: 13149,
min: 13149,
},
}
for _, testcase := range cases {
t.Run(testcase.name, func(t *testing.T) {
genField := &GenField{
Type: "timestamp",
minStepDuration: time.Second,
maxStepDuration: time.Second * 2,
TimeFormat: unixFormat,
TimeUnit: testcase.unit,
Distribution: "increasing",
MinDate: time.Date(2006, time.January, 1, 0, 0, 0, 0, time.UTC),
MaxDate: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC),
}
r := rand.New(rand.NewSource(3))
gen, err := genField.Generator(r)
if err != nil {
t.Fatalf("making generator: %v", err)
}
rec, err := gen.Generate(nil)
if err != nil {
t.Fatalf("generating: %v", err)
}
if recInt, ok := rec.(int64); !ok {
t.Fatalf("expected int64, but got %v of %[1]T", recInt)
} else if recInt < testcase.min || recInt > testcase.max {
t.Fatalf("unexpected value for time: %v", recInt)
}
})
}
}
func TestShiftingStringGenerator(t *testing.T) {
// max < min
if ssg, err := NewShiftingStringGenerator(&GenField{MinLen: 10, MaxLen: 9}, nil); err == nil {
t.Fatalf("expected error, but got %+v", ssg)
} else if !strings.Contains(err.Error(), "must be greater than or equal to") {
t.Fatalf("unexpected error: %v", err)
}
// neg step
if ssg, err := NewShiftingStringGenerator(&GenField{Step: -1}, nil); err == nil {
t.Fatalf("expected error, but got %+v", ssg)
} else if !strings.Contains(err.Error(), "'step' for shifting must be positive") {
t.Fatalf("unexpected error: %v", err)
}
// min zero but max not
if ssg, err := NewShiftingStringGenerator(&GenField{MaxLen: 7, Step: 2}, rand.New(rand.NewSource(1))); err != nil {
t.Fatalf("unexpected error %v", err)
} else if ssg.lenVariance != 0 {
t.Fatalf("expected zero variance, but got: %+v", ssg)
}
// max zero but min not
if ssg, err := NewShiftingStringGenerator(&GenField{MinLen: 7}, rand.New(rand.NewSource(1))); err != nil {
t.Fatalf("unexpected error %v", err)
} else if ssg.lenVariance != 0 {
t.Fatalf("expected zero variance, but got: %+v", ssg)
} else if ssg.step != 1 {
t.Fatalf("expected step 0 but got: %+v", ssg)
}
ssg, err := NewShiftingStringGenerator(&GenField{MinLen: 5, MaxLen: 10, Cardinality: 4, Step: 20}, rand.New(rand.NewSource(1)))
if err != nil {
t.Fatalf("unexpected error: %+v", err)
}
vals := make([]string, 0, 40)
valMap := make(map[string]struct{})
for i := 0; i < 20; i++ {
val, err := ssg.Generate(nil)
if err != nil {
t.Fatalf("unexpected generation err: %v", err)
}
valStr := val.(string)
vals = append(vals, valStr)
valMap[valStr] = struct{}{}
if len(valStr) < 5 || len(valStr) > 10 {
t.Fatalf("got unexpected length val at position: %d, %v", i, vals)
}
}
if len(valMap) != 4 {
t.Fatalf("unexpected cardinality: %v", valMap)
}
for i := 20; i < 40; i++ {
val, err := ssg.Generate(nil)
if err != nil {
t.Fatalf("unexpected generation err: %v", err)
}
valStr := val.(string)
vals = append(vals, valStr)
delete(valMap, valStr)
if len(valStr) < 5 || len(valStr) > 10 {
t.Fatalf("got unexpected length val at position: %d, %v", i, vals)
}
}
if len(valMap) != 1 {
t.Fatalf("should have a single value from the first 20 that isn't generated in second 20, but have valMap:\n%v\nvals:\n%v", valMap, vals)
}
}

106
idk/datagen/customer.go Normal file
View file

@ -0,0 +1,106 @@
package datagen
import (
"io"
"math/rand"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
)
// Ensure Customer implements interface.
var _ Sourcer = (*Customer)(nil)
// Customer implements Sourcer.
type Customer struct{}
// NewCustomer returns a new instance of Customer.
func NewCustomer(cfg SourceGeneratorConfig) Sourcer {
return &Customer{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (c *Customer) Source(cfg SourceConfig) idk.Source {
src := &CustomerSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(22)),
schema: []idk.Field{
idk.IDField{NameVal: "id"},
idk.StringField{NameVal: "region"},
idk.IntField{NameVal: "size"},
idk.IDField{NameVal: "sales_channel"},
},
}
src.record = make([]interface{}, len(src.schema))
src.regionZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(regions))-1)
src.record[0] = cfg.startFrom
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (c *Customer) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (c *Customer) DefaultEndAt() uint64 {
return 100 // TODO: what should this be?
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (c *Customer) Info() string {
return "TODO"
}
// Ensure CustomerSource implements interface.
var _ idk.Source = (*CustomerSource)(nil)
type CustomerSource struct {
Log logger.Logger
cur, endAt uint64
rand *rand.Rand
regionZipf *rand.Zipf
schema []idk.Field
record record
}
var regions = []string{"US", "Canada", "Central America", "South America", "Western Europe", "Eastern Europe", "Africa", "China", "Japan", "Russia", "Middle East", "Pacific Islands", "Antarctica"}
func (s *CustomerSource) Record() (idk.Record, error) {
if s.cur >= s.endAt {
return nil, io.EOF
}
s.record[0] = s.cur
s.record[1] = regions[s.regionZipf.Uint64()]
s.record[2] = s.rand.Intn(24990) + 10
s.record[3] = uint64(s.rand.Intn(500))
s.cur++
return s.record, nil
}
func (s *CustomerSource) Schema() []idk.Field {
return s.schema
}
func (s *CustomerSource) Seed(seed int64) {
s.rand.Seed(seed)
}
var _ Seedable = (*CustomerSource)(nil)
func (s *CustomerSource) Close() error {
return nil
}

View file

@ -0,0 +1,603 @@
package datagen
import (
"fmt"
"io"
"math/rand"
"sort"
"strings"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/datagen/gen"
)
/*
go run cmd/datagen/main.go \
--source=customer_segmentation --pilosa.index=customer_segmentation \
--start-from=1 --end-at=100 --pilosa.batch-size=100
go run cmd/datagen/main.go \
--source=example --pilosa.index=example \
--start-from=1 --end-at=100 --pilosa.batch-size=100
*/
// Ensure Person implements interface.
var _ Sourcer = (*Person)(nil)
// Person implements Sourcer,
type Person struct{}
// NewPerson returns a new instance of Person.
func NewPerson(cfg SourceGeneratorConfig) Sourcer {
return &Person{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (p *Person) Source(cfg SourceConfig) idk.Source {
g := gen.New(gen.OptGenSeed(cfg.seed))
src := &PersonSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.StringField{NameVal: "sex"}, // 1
idk.IntField{NameVal: "age"}, // 2
idk.StringField{NameVal: "race"}, // 3
idk.BoolField{NameVal: "hispanic"}, // 4
idk.StringField{NameVal: "political_party_affiliation"}, // 5
idk.StringArrayField{NameVal: "education"}, // 6
idk.IntField{NameVal: "income"}, // 7
idk.IDField{NameVal: "general_election_voting_frequency"}, // 8
idk.IDField{NameVal: "primary_election_voting_frequency"}, // 9
idk.StringArrayField{NameVal: "hobbies"}, // 10
},
g: g,
generatePerson: getGenRandPerson(g.R),
hobbyStash: []string{},
}
src.record = make([]interface{}, len(src.schema))
src.record[0] = uint64(0)
src.record[1] = make([]byte, 12)
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (e *Person) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (e *Person) DefaultEndAt() uint64 {
return 100
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (p *Person) Info() string {
return "Generates data for customer segmentation."
}
// Ensure PersonSource implements interface.
var _ idk.Source = (*PersonSource)(nil)
// PersonSource is an instance of a source generated
// by the Sourcer implementation Person.
type PersonSource struct {
cur uint64
endAt uint64
schema []idk.Field
record record
g *gen.Gen
generatePerson func() *person
hobbyStash []string
}
// Record implements idk.Source.
func (ps *PersonSource) Record() (idk.Record, error) {
if ps.cur >= ps.endAt {
return nil, io.EOF
}
p := ps.generatePerson()
ps.setRandomHobbies()
// Populate fields
ps.record[0] = ps.cur
ps.record[1] = p.sex
ps.record[2] = p.age
ps.record[3] = p.race
ps.record[4] = p.hispanic
ps.record[5] = p.politicalPartyAffiliation
ps.record[6] = educationLevelToStringArr(p.education)
ps.record[7] = p.income
ps.record[8] = p.generalElectionVotingFrequency
ps.record[9] = p.primaryElectionVotingFrequency
ps.record[10] = ps.hobbyStash
// Increment the ID.
ps.cur++
return ps.record, nil
}
func (ps *PersonSource) setRandomHobbies() {
ps.hobbyStash = ps.g.StringSliceFromListWeighted(ps.hobbyStash, hobbies, 2, 5)
}
// Schema implements idk.Source.
func (ps *PersonSource) Schema() []idk.Field {
return ps.schema
}
// ----------------------------------------------------
// THIS IS WHERE THE MAGIC HAPPENS
// ----------------------------------------------------
type randValue struct {
value interface{}
probability float64 // probability of being picked, all in set should add to 1.0
count int // number of times val has been picked so far
}
type randVarPickerFn func() interface{}
type getStatsFn func() string
func getRandVarPicker(rng *rand.Rand, vals []randValue) (randVarPickerFn, getStatsFn) {
cumulativeSums := make([]float64, len(vals))
currSum := float64(0)
for i, v := range vals {
currSum += v.probability
cumulativeSums[i] = currSum
}
// call stats at any time to get, well, stats
stats := func() string {
// make copy to avoid modifying original
valsCopy := make([]randValue, len(vals))
copy(valsCopy, vals)
// sort counts
sort.Slice(valsCopy, func(i, j int) bool {
return valsCopy[i].count > valsCopy[j].count
})
// get total gen so far
total := 0
for _, v := range valsCopy {
total += v.count
}
// gen str summary
var sb strings.Builder
for _, v := range valsCopy {
percentage := (float64(v.count) / float64(total)) * 100
s := fmt.Sprintf("%v (%d/%d) %0.2f%%\n", v.value, v.count, total, percentage)
sb.WriteString(s)
}
return sb.String()
}
picker := func() interface{} {
rv := rng.Float64()
i := sort.SearchFloat64s(cumulativeSums, rv)
// note that if the probabilities do not add up to 1.0
// i will == len(cumulativeSums), hence out of bounds error
// however, floating points are tricky and require special care
// so I will assume that the probabilities provided were meant to
// add closely up to 1.0 (eg it ended up being 0.999900)
// and if the rv generated is for example 0.9999000011, the last
// value of the slice will be picked instead. In future, there
// should be an assertion on the vals slice probabilities to ensure
// they add up to 1 +/- some epsilon value.
if i == len(vals) {
i = len(vals) - 1
}
vals[i].count++
return vals[i].value
}
return picker, stats
}
type numericRange struct {
min int // inclusive
max int // inclusive
}
type raceEthnicity struct {
race string
isHispanic bool
}
type educationLevel struct {
hasCompletedHighSchool bool
hasAttendedSomeCollege bool
hasCompletedBachelors bool
hasCompletedGraduate bool
}
var cG []string = []string{"High school diploma or GED", "Some college", "Bachelor's degree", "Doctorate and/or Professional degree"}
var cB []string = []string{"High school diploma or GED", "Some college", "Bachelor's degree"}
var cASG []string = []string{"High school diploma or GED", "Some college"}
var cHS []string = []string{"High school diploma or GED"}
func educationLevelToStringArr(e educationLevel) []string {
if e.hasCompletedGraduate {
return cG
}
if e.hasCompletedBachelors {
return cB
}
if e.hasAttendedSomeCollege {
return cASG
}
if e.hasCompletedHighSchool {
return cHS
}
return nil
}
func pickRandNumFromRange(rng *rand.Rand, nr numericRange) int {
upperBoundExclusive := int32((nr.max - nr.min) + 1)
rv := int(rng.Int31n(upperBoundExclusive)) // [0, upperBoundExclusive)
return rv + nr.min
}
type person struct {
sex string
age int
race string
hispanic bool
politicalPartyAffiliation string
education educationLevel
income int
generalElectionVotingFrequency int
primaryElectionVotingFrequency int
}
func getGenRandPerson(rng *rand.Rand) func() *person {
pickSex, _ := getRandVarPicker(rng, []randValue{
{
value: "Male",
probability: 0.475,
},
{
value: "Female",
probability: 0.475,
},
{
value: "Unspecified",
probability: 0.05,
},
})
pickAge, _ := getRandVarPicker(rng, []randValue{
{
value: numericRange{
min: 0,
max: 14,
},
probability: 0.1873,
},
{
value: numericRange{
min: 15,
max: 24,
},
probability: 0.1327,
}, {
value: numericRange{
min: 25,
max: 54,
},
probability: 0.3945,
},
{
value: numericRange{
min: 55,
max: 64,
},
probability: 0.1291,
},
{
value: numericRange{
min: 65,
max: 100,
},
probability: 0.1563,
},
})
// 75.72% (53.89% Non-Hispanic) White
// 16.06% (14.77% Non-Hispanic) African American
// 7.09% Asian / Pacific Islander
// 1.13% Native American / Alaskan Native
pickRaceAndEthnicity, _ := getRandVarPicker(rng, []randValue{
{
value: raceEthnicity{
race: "White",
isHispanic: false,
},
probability: 0.5389,
},
{
value: raceEthnicity{
race: "White",
isHispanic: true,
},
probability: 0.2183,
},
{
value: raceEthnicity{
race: "African American",
isHispanic: false,
},
probability: 0.1477,
},
{
value: raceEthnicity{
race: "African American",
isHispanic: true,
},
probability: 0.0129,
},
{
value: raceEthnicity{
race: "Asian / Pacific Islander",
isHispanic: false,
},
probability: 0.0709,
},
{
value: raceEthnicity{
race: "Native American / Alaskan Native",
isHispanic: false,
},
probability: 0.0113,
},
})
// Democrat 27%
// Republican 20%
// Independent 19%
// Democrat & Independent 6%
// Republican & Independent 5%
// Democrat & Republican 4%
// Democrat & Republican & Independent 1%
// None Reported/ Declined to reply 18%
pickPoliticalPartyAffiliation, _ := getRandVarPicker(rng, []randValue{
{
value: "Democrat",
probability: 0.27,
},
{
value: "Republican",
probability: 0.20,
},
{
value: "Independent",
probability: 0.19,
},
{
value: "Democrat and Independent",
probability: 0.06,
},
{
value: "Republican and Independent",
probability: 0.05,
},
{
value: "Democrat and Republican",
probability: 0.04,
},
{
value: "Democrat and Republican and Independent",
probability: 0.01,
},
{
value: "",
probability: 0.18,
},
})
// 89.80% High school diploma or GED
// 61.28% Some college
// 34.98% Bachelor's degree
// 13.04% Master's and/or doctorate and/or professional degree
// Percentages are additive, meaning that 89.80% High School/GED
// includes everyone with that education level or higher.
// Let
// H = HighSchool
// SC = SomeCollege
// B = Bachelor's
// G = Graduate/Professional
// P(H) = 0.8980
// P(H & SC) = 0.6128
// P(H & SC & B) = 0.3498
// P(H & SC & B & G) = 0.1304
// P(SC | H) = P(H & SC) / P(H) = 0.6824
// P(B | H & SC) = P(H & SC & B) / P(H & SC) = 0.5708
// P(G | H & SC & B) = P(H & SC & B & G) / P(SC & B & G) = 0.3728
pickEducationLevel := func(rng *rand.Rand) educationLevel {
var ed educationLevel
if rng.Float64() > 0.8980 {
return ed
}
ed.hasCompletedHighSchool = true
if rng.Float64() > 0.6824 {
return ed
}
ed.hasAttendedSomeCollege = true
if rng.Float64() > 0.5708 {
return ed
}
ed.hasCompletedBachelors = true
if rng.Float64() > 0.3728 {
return ed
}
ed.hasCompletedGraduate = true
return ed
}
// 9.1% $0-$15000
// 8% $15000-$24999
// 8.3% $25000-$34999
// 11.7% $35000-$49999
// 16.5% $50000-$74999
// 12.3% 75000-99999
// 15.5% 100000-149999
// 8.3% 150000-199999
// 10.3% $200000+
pickIncome, _ := getRandVarPicker(rng, []randValue{
{
value: numericRange{
min: 0,
max: 15000,
},
probability: 0.091,
},
{
value: numericRange{
min: 15000,
max: 24999,
},
probability: 0.08,
}, {
value: numericRange{
min: 25000,
max: 34999,
},
probability: 0.083,
},
{
value: numericRange{
min: 35000,
max: 49999,
},
probability: 0.117,
},
{
value: numericRange{
min: 50000,
max: 74999,
},
probability: 0.165,
},
{
value: numericRange{
min: 75000,
max: 99999,
},
probability: 0.123,
},
{
value: numericRange{
min: 100000,
max: 149999,
},
probability: 0.155,
},
{
value: numericRange{
min: 150000,
max: 199999,
},
probability: 0.083,
},
{
value: numericRange{
min: 200000,
max: 500000,
},
probability: 0.103,
},
})
// 0 -> 21%
// 1 -> 17%
// 2 -> 17%
// 3 -> 15%
// 4 -> 38%
pickGeneralElectionVotingFrequency, _ := getRandVarPicker(rng, []randValue{
{
value: 0,
probability: 0.21,
},
{
value: 1,
probability: 0.17,
},
{
value: 2,
probability: 0.17,
},
{
value: 3,
probability: 0.15,
},
{
value: 4,
probability: 0.38,
},
})
// 0 -> 51%
// 1 -> 19%
// 2 -> 11%
// 3 -> 7%
// 4 -> 11%
pickPrimaryElectionVotingFrequency, _ := getRandVarPicker(rng, []randValue{
{
value: 0,
probability: 0.51,
},
{
value: 1,
probability: 0.19,
},
{
value: 2,
probability: 0.11,
},
{
value: 3,
probability: 0.07,
},
{
value: 4,
probability: 0.11,
},
})
return func() *person {
p := &person{}
p.sex = pickSex().(string)
p.age = pickRandNumFromRange(rng, pickAge().(numericRange))
raceAndEthnicity := pickRaceAndEthnicity().(raceEthnicity)
p.race = raceAndEthnicity.race
p.hispanic = raceAndEthnicity.isHispanic
p.politicalPartyAffiliation = pickPoliticalPartyAffiliation().(string)
p.education = pickEducationLevel(rng)
p.income = pickRandNumFromRange(rng, pickIncome().(numericRange))
p.generalElectionVotingFrequency = pickGeneralElectionVotingFrequency().(int)
p.primaryElectionVotingFrequency = pickPrimaryElectionVotingFrequency().(int)
return p
}
}
func (ps *PersonSource) Close() error {
return nil
}

View file

@ -0,0 +1,95 @@
package datagen
import (
"io"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/datagen/gen"
)
var _ Sourcer = (*LinkedInPerson)(nil)
type LinkedInPerson struct{}
func NewLinkedInPerson(cfg SourceGeneratorConfig) Sourcer {
return &LinkedInPerson{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (p *LinkedInPerson) Source(cfg SourceConfig) idk.Source {
g := gen.New(gen.OptGenSeed(cfg.seed))
src := &LinkedInPersonSource{
startFrom: cfg.startFrom,
endAt: cfg.endAt,
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.StringField{NameVal: "city"}, // 1
idk.StringField{NameVal: "zip_code"}, // 2
idk.StringArrayField{NameVal: "skills"}, // 3
idk.StringArrayField{NameVal: "titles"}, // 4
},
g: g,
}
src.record = make([]interface{}, len(src.schema))
src.record[3] = []string{}
src.record[4] = []string{}
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (e *LinkedInPerson) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (e *LinkedInPerson) DefaultEndAt() uint64 {
return 100
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (p *LinkedInPerson) Info() string {
return "Simulates adding LinkedIn data to existing customer segmentation data."
}
var _ idk.Source = (*LinkedInPersonSource)(nil)
type LinkedInPersonSource struct {
count uint64
startFrom uint64
endAt uint64
schema []idk.Field
record record
g *gen.Gen
}
func (lp *LinkedInPersonSource) Record() (idk.Record, error) {
lp.count++
if lp.count > lp.endAt-lp.startFrom {
return nil, io.EOF
}
lp.record[0] = uint64(lp.g.R.Int63n(int64(lp.endAt-lp.startFrom))) + lp.startFrom
lp.record[1] = lp.g.StringFromList(uscities)
lp.record[2] = lp.g.StringFromList(zip_codes)
lp.record[3] = lp.g.StringSliceFromListWeighted(lp.record[3].([]string), skills, 2, 5)
lp.record[4] = lp.g.StringSliceFromListWeighted(lp.record[4].([]string), titles, 2, 5)
return lp.record, nil
}
func (lp *LinkedInPersonSource) Schema() []idk.Field {
return lp.schema
}
func (lp *LinkedInPersonSource) Close() error {
return nil
}

View file

@ -0,0 +1,27 @@
package datagen
import (
"testing"
)
func TestPersonHobbies(t *testing.T) {
p := NewPerson(SourceGeneratorConfig{})
ps := p.Source(SourceConfig{endAt: 10})
i := 0
for i = 0; i < 10; i++ {
rec, err := ps.Record()
if err != nil {
t.Fatalf("getting record: %v", err)
}
data := rec.Data()
if dataStringSlice, ok := data[10].([]string); !ok {
t.Fatalf("expected string slice, but got type:%T, data:%[1]v", data[10])
} else if len(dataStringSlice) > 0 {
break // we just want to see one that has data in it, then we'll call that success
}
}
if i == 10 {
t.Fatalf("never saw any generated hobbies (slice was always empty)")
}
}

559
idk/datagen/datagen_test.go Normal file
View file

@ -0,0 +1,559 @@
package datagen
import (
"fmt"
"io"
"math/rand"
"os"
"sort"
"sync"
"testing"
"time"
"github.com/confluentinc/confluent-kafka-go/kafka"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/common"
)
var sources = []string{
"bank",
"claim",
"custom",
"customer",
"dell",
"dwarranty",
"equipment",
"hughes",
"item",
"kitchensink",
"kitchensink_keyed",
"merck",
"network_ts",
"power_scenario_1",
"power_scenario_1_2",
"power_ts",
"site",
"sizing",
"stringpk",
"texas_health",
"transactions_scenario_1",
"transactions_ts",
"warranty",
}
var keyedIndices = map[string]bool{
"stringpk": true,
"kitchensink_keyed": true,
"bank": true,
}
var duplicateIndices = map[string]bool{
"bank": true,
"customer_segmentation_linkedin": true,
"custom": true,
}
// TestConfig tests that all default configurations load.
func TestConfig(t *testing.T) {
t.Parallel()
for _, src := range sources {
src := src
t.Run(src, func(t *testing.T) {
t.Parallel()
main := NewMain()
main.Source = src
main.Concurrency = 2
if src == "custom" {
main.CustomConfig = "testdata/custom.yaml"
}
main.Target = TargetFeaturebase
err := main.Preload()
if err != nil {
t.Errorf("failed to preload: %v", err)
return
}
main.NoStats()
main.PrintPlan()
})
}
}
// TestGen tests record generation.
// It ensures that all generated IDs are unique, and that the right number of records are generated.
// If the IDs are numerical, it also ensures that they occupy a single contiguous range.
func TestGen(t *testing.T) {
t.Parallel()
for _, src := range sources {
src := src
t.Run(src, func(t *testing.T) {
t.Parallel()
const conc = 16
const n = 100
main := NewMain()
main.Source = src
main.Concurrency = conc
main.EndAt = n - 1
if src == "custom" {
main.CustomConfig = "testdata/custom.yaml"
}
if err := main.Preload(); err != nil {
t.Errorf("failed to preload: %v", err)
return
}
main.NoStats()
var wg sync.WaitGroup
var out [conc]map[interface{}]struct{}
var errs [conc]error
srcs := main.Sources()
for i := range out {
i := i
wg.Add(1)
go func() {
defer wg.Done()
ids := map[interface{}]struct{}{}
out[i] = ids
for {
rec, err := srcs[i].Record()
if err != nil {
if err == io.EOF {
return
}
errs[i] = err
return
}
id := rec.Data()[0]
if idbytes, ok := id.([]byte); ok {
id = string(idbytes)
}
if _, ok := ids[id]; ok {
errs[i] = fmt.Errorf("duplicated ID: %v", id)
return
}
ids[id] = struct{}{}
}
}()
}
wg.Wait()
for i, v := range errs {
if v != nil {
t.Errorf("source %d failed: %v", i, v)
}
}
total := 0
ids := map[interface{}][]int{}
var idlist []interface{}
for i, s := range out {
total += len(s)
for id := range s {
if _, ok := ids[id]; !ok {
idlist = append(idlist, id)
}
ids[id] = append(ids[id], i)
}
}
// some sources are expected to have duplicate records, so skip checking for duplicates.
if !duplicateIndices[src] {
sort.Slice(idlist, func(i, j int) bool {
switch idlist[i].(type) {
case uint64:
return idlist[i].(uint64) < idlist[j].(uint64)
case string:
return idlist[i].(string) < idlist[j].(string)
default:
panic(fmt.Errorf("unexpected type: %T", idlist[i]))
}
})
for _, id := range idlist {
srcs := ids[id]
if len(srcs) > 1 {
sort.Ints(srcs)
t.Errorf("duplicates of id %v found in sources: %v", id, srcs)
}
}
if base, ok := idlist[0].(uint64); ok {
for i, v := range idlist {
if base+uint64(i) != v {
t.Errorf("found gap in ID range at %d", base+uint64(i))
break
}
}
}
}
if total != n {
t.Errorf("expected %d records; got %d records", n, total)
}
})
}
}
func configureTestFlags(main *Main) {
if pilosaHost, ok := os.LookupEnv("IDK_TEST_PILOSA_HOST"); ok {
main.Pilosa.Hosts = []string{pilosaHost}
} else {
main.Pilosa.Hosts = []string{"pilosa:10101"}
}
}
func TestThousandFormating(t *testing.T) {
t.Parallel()
tests := []struct {
input uint64
output string
}{
{input: 0, output: "0"},
{input: 1000, output: "1,000"},
{input: 111222333444, output: "111,222,333,444"},
}
for _, tc := range tests {
tc := tc
if tc.output != AddThousandSep(tc.input) {
t.Errorf("expected %v but got %v", tc.output, AddThousandSep(tc.input))
}
}
}
// TestIntegration is an intergration test that ingests each source into Pilosa.
// It currently only verifies that the correct number of rows are ingested into each index.
func TestIntegration(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping integration tests in short mode")
return
}
for _, src := range sources {
src := src
t.Run(src, func(t *testing.T) {
t.Parallel()
const n = 100
main := NewMain()
configureTestFlags(main)
main.Pilosa.Index = fmt.Sprintf("test-%s-%d", src, rand.New(rand.NewSource(time.Now().Unix())).Uint64())
main.Source = src
main.Target = TargetFeaturebase
main.EndAt = n - 1
main.Concurrency = 3
main.Pilosa.BatchSize = 13
if src == "custom" {
main.CustomConfig = "testdata/custom.yaml"
}
if err := main.Preload(); err != nil {
t.Errorf("failed to preload: %v", err)
return
}
main.NoStats()
defer func() {
client := main.PilosaClient()
if client == nil {
return
}
if err := client.DeleteIndexByName(main.Pilosa.Index); err != nil {
t.Errorf("deleting test index: %v", err)
}
}()
if err := main.Run(); err != nil {
t.Errorf("failed to ingest: %v", err)
return
}
client := main.PilosaClient()
schema, err := client.Schema()
if err != nil {
t.Errorf("getting schema: %v", err)
return
}
index := schema.Indexes()[main.Pilosa.Index]
if index == nil {
t.Error("index dissapeared")
return
}
expectKeys := keyedIndices[src]
indexKeys := index.Opts().Keys()
if indexKeys != expectKeys {
t.Errorf("expected keys=%v; got keys=%v", expectKeys, indexKeys)
}
resp, err := client.Query(index.Count(index.All()))
if err != nil {
t.Errorf("validation query failed: %v", err)
return
}
recordCount := resp.Result().Count()
// some sources have duplicate records with same id.
// record count in featurebase won't match the number of ingested records
if duplicateIndices[src] {
if recordCount == 0 {
t.Errorf("expected %d records; found %d records", 0, recordCount)
}
} else {
if recordCount != n {
t.Errorf("expected %d records; found %d records", n, recordCount)
}
}
})
}
}
func TestConfigKafka(t *testing.T) {
t.Parallel()
tests := []struct {
src string
conc int
numpartitions int
replicationfactor int
topic string
batchsize int
target string
}{
{
src: sources[0],
conc: 2,
numpartitions: 4,
replicationfactor: 2,
topic: fmt.Sprintf("topic-%d", rand.New(rand.NewSource(time.Now().Unix())).Uint64()),
batchsize: 1000000,
target: TargetKafka,
},
{
src: sources[0],
conc: 1,
numpartitions: 1,
replicationfactor: 1,
topic: fmt.Sprintf("topic-%d", rand.New(rand.NewSource(time.Now().Unix())).Uint64()),
batchsize: 10000,
target: TargetKafkaStatic,
},
{
src: sources[0],
conc: 3,
numpartitions: 1,
replicationfactor: 1,
topic: fmt.Sprintf("topic-%d", rand.New(rand.NewSource(time.Now().Unix())).Uint64()),
batchsize: 10000,
target: TargetKafkaStatic,
},
}
for i, test := range tests {
tc := test
t.Run(fmt.Sprintf("Test%d", i), func(t *testing.T) {
t.Parallel()
main := NewMain()
main.Source = tc.src
main.Concurrency = tc.conc
main.Target = tc.target
main.Concurrency = tc.conc
main.Kafka.BatchSize = tc.batchsize
main.Kafka.Topic = tc.topic
main.Kafka.ReplicationFactor = tc.replicationfactor
main.Kafka.NumPartitions = tc.numpartitions
err := main.Preload()
if err != nil {
t.Fatalf("failed to preload: %v", err)
}
if main.KafkaPut.Concurrency != tc.conc {
t.Fatalf("expected %d for concurrency, got %d", tc.conc, main.KafkaPut.Concurrency)
}
if main.KafkaPut.BatchSize != tc.batchsize {
t.Fatalf("expected %d for batch-size, got %d", tc.batchsize, main.KafkaPut.BatchSize)
}
if main.KafkaPut.NumPartitions != tc.numpartitions {
t.Fatalf("expected %d for num partitions, got %d", tc.numpartitions, main.KafkaPut.NumPartitions)
}
if main.KafkaPut.ReplicationFactor != tc.replicationfactor {
t.Fatalf("expected %d for replication factor, got %d", tc.replicationfactor, main.KafkaPut.ReplicationFactor)
}
if main.KafkaPut.Target != tc.target {
t.Fatalf("expected %s for target, got %s", tc.target, main.KafkaPut.Target)
}
if main.KafkaPut.Topic != tc.topic {
t.Fatalf("expected %s for topic, got %s", tc.topic, main.KafkaPut.Topic)
}
})
}
}
func configureKafkaTestFlags(main *Main) {
main.Pilosa.Hosts = []string{"pilosa:10101"}
main.Kafka.KafkaBootstrapServers = []string{"kafka:9092"}
}
func TestIntegrationWithKafka(t *testing.T) {
tests := []struct {
target string
numPartitions int
primaryKeyFields []string
idField string
}{
{
target: TargetKafka,
numPartitions: 6,
primaryKeyFields: []string{"uuid"},
idField: "uuid",
},
{
target: TargetKafka,
numPartitions: 1,
primaryKeyFields: []string{},
idField: "",
},
{
target: TargetKafkaStatic,
numPartitions: 4,
primaryKeyFields: []string{},
idField: "",
},
{
target: TargetKafkaStatic,
numPartitions: 2,
primaryKeyFields: []string{"uuid"},
idField: "uuid",
},
}
for i, test := range tests {
tc := test
ti := i
t.Run(tc.target, func(t *testing.T) {
const n = 100
src := "custom"
main := NewMain()
main.Pilosa.Index = fmt.Sprintf("test-%s-%d-%d", tc.target, ti, rand.New(rand.NewSource(time.Now().Unix())).Uint64())
main.Source = src
main.CustomConfig = "testdata/tremor_keys.yaml"
main.Target = tc.target
main.EndAt = n - 1
main.Pilosa.BatchSize = 13
configureKafkaTestFlags(main)
main.Kafka.Topic = fmt.Sprintf("topic-%s-%d-%d", tc.target, ti, rand.New(rand.NewSource(time.Now().Unix())).Uint64())
main.Kafka.NumPartitions = tc.numPartitions
main.Concurrency = tc.numPartitions // make concurrency and partitions equal
main.Kafka.ReplicationFactor = 1
if tc.target == TargetKafka {
main.Kafka.SchemaRegistryURL = "http://schema-registry:8081"
}
if len(tc.primaryKeyFields) != 0 {
main.idkMain.PrimaryKeyFields = tc.primaryKeyFields
}
if tc.idField != "" {
main.idkMain.IDField = tc.idField
}
if err := main.Preload(); err != nil {
t.Fatalf("failed to preload: %v", err)
}
if err := main.KafkaPut.Run(); err != nil {
t.Fatalf("failed to ingest records to kafka: %v", err)
}
if err := main.KafkaPut.ConfigMap.SetKey("go.produce.channel.size", 1); err != nil {
t.Fatalf("failed to st produce channel size: %v", err)
}
confluentCommand := idk.ConfluentCommand{
KafkaBootstrapServers: []string{"kafka:9092"},
KafkaSocketTimeoutMs: 10,
}
configMap, err := common.SetupConfluent(&confluentCommand)
if err != nil {
t.Fatalf("failed to create kafka confluent config map: %v", err)
}
kafkaClient, err := kafka.NewAdminClient(configMap)
if err != nil {
t.Fatalf("failed to get kafka client: %v", err)
}
defer kafkaClient.Close()
metadata, err := kafkaClient.GetMetadata(&main.KafkaPut.Topic, false, 100)
if err != nil {
t.Fatalf("failed to get kafka metadata: %v", err)
}
topic, ok := metadata.Topics[main.KafkaPut.Topic]
if !ok {
t.Fatalf("topic %s not found in metadata", main.KafkaPut.Topic)
}
if len(topic.Partitions) != tc.numPartitions {
t.Fatalf("expected %d for num partitions, got %d", tc.numPartitions, len(topic.Partitions))
}
})
}
}
func TestStartEnds(t *testing.T) {
tests := []struct {
config SourceGeneratorConfig
conc int
ses []startEnd
total uint64
}{
{
config: SourceGeneratorConfig{
StartFrom: 0,
EndAt: 100,
Concurrency: 5},
conc: 5,
ses: []startEnd{{0, 20}, {20, 40}, {40, 60}, {60, 80}, {80, 100}},
total: 100,
},
{
config: SourceGeneratorConfig{
StartFrom: 0,
EndAt: 100,
Concurrency: 1},
conc: 1,
ses: []startEnd{{0, 100}},
total: 100,
},
}
for i, test := range tests {
tc := test
t.Run(fmt.Sprintf("Test_%d", i), func(t *testing.T) {
conc, ses, total := startEnds(tc.config)
if conc != tc.conc {
t.Fatalf("expected %d for concurrency, got %d", tc.conc, conc)
}
if total != tc.total {
t.Fatalf("expected %d for total records, got %d", tc.total, total)
}
if len(ses) != len(tc.ses) {
t.Fatalf("expected %d of boundaries, got %d", len(tc.ses), len(ses))
}
for i, exp := range tc.ses {
got := ses[i]
if exp.start != got.start || exp.end != got.end {
t.Fatalf("expected boundaries to start %d and end at %d, but got start %d and end at %d", exp.start, exp.end, got.start, got.end)
}
}
})
}
}

22
idk/datagen/dell.data.go Normal file

File diff suppressed because one or more lines are too long

171
idk/datagen/dell.go Normal file
View file

@ -0,0 +1,171 @@
package datagen
import (
"io"
"math/rand"
"github.com/molecula/featurebase/v3/idk"
)
// Ensure Dell implements interface.
var _ Sourcer = (*Dell)(nil)
// Dell implements Sourcer, and returns a very basic
// data set. It can be used as an Dell for writing
// additional custom Sourcers.
type Dell struct{}
// NewDell returns a new instance of Dell.
func NewDell(cfg SourceGeneratorConfig) Sourcer {
return &Dell{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (e *Dell) Source(cfg SourceConfig) idk.Source {
src := &DellSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(19)),
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.StringField{NameVal: "service_tag"}, // 1
idk.IntField{NameVal: "record_date", Min: intptr(10000), Max: intptr(30000)}, // 2
idk.StringField{NameVal: "system_model"}, // 3
idk.StringField{NameVal: "bios_version"}, // 4
idk.StringField{NameVal: "operating_system"}, // 5
idk.IntField{NameVal: "system_ram_gb", Min: intptr(1), Max: intptr(256)}, // 6
idk.IntField{NameVal: "video_fan_speed", Min: intptr(0), Max: intptr(20)}, // 7
idk.IntField{NameVal: "proc_fan_speed", Min: intptr(0), Max: intptr(20)}, // 8
idk.StringField{NameVal: "epsa_fail_code"}, // 9
idk.StringField{NameVal: "epsa_version"}, // 10
idk.StringField{NameVal: "video_controller"}, // 11
idk.StringField{NameVal: "ddv_revision"}, // 12
idk.StringField{NameVal: "motherboard_eppid"}, // 13
idk.IntField{NameVal: "proc_fan_avg", Min: intptr(0), Max: intptr(30)}, // 14
idk.StringField{NameVal: "proc_information"}, // 15
},
}
src.system_modelZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(system_model))-1)
src.operating_systemZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(operating_system))-1)
src.system_ramZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(system_ram))-1)
src.epsa_fail_codeZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(epsa_fail_code))-1)
src.epsa_versionZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(epsa_version))-1)
src.video_controllerZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(video_controller))-1)
src.ddv_revisionZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(ddv_revision))-1)
src.motherboard_eppidZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(motherboard_eppid))-1)
src.processor_informationZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(processor_information))-1)
src.bios_versionZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(bios_version))-1)
src.record = make([]interface{}, len(src.schema))
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (e *Dell) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (e *Dell) DefaultEndAt() uint64 {
return 100
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (e *Dell) Info() string {
return "Generates data representative of Dell's internal hardware tracking operations."
}
// Ensure DellSource implements interface.
var _ idk.Source = (*DellSource)(nil)
// DellSource is an instance of a source generated
// by the Sourcer implementation Dell.
type DellSource struct {
cur uint64
endAt uint64
rand *rand.Rand
system_modelZipf *rand.Zipf
operating_systemZipf *rand.Zipf
system_ramZipf *rand.Zipf
epsa_fail_codeZipf *rand.Zipf
epsa_versionZipf *rand.Zipf
video_controllerZipf *rand.Zipf
ddv_revisionZipf *rand.Zipf
motherboard_eppidZipf *rand.Zipf
processor_informationZipf *rand.Zipf
bios_versionZipf *rand.Zipf
schema []idk.Field
record record
}
// Record implements idk.Source.
func (e *DellSource) Record() (idk.Record, error) {
if e.cur >= e.endAt {
return nil, io.EOF
}
e.record[0] = e.cur
e.record[1] = e.StringDell(7)
e.record[2] = e.generateRandomInt(30000, 10000)
e.record[3] = system_model[e.system_modelZipf.Uint64()]
e.record[4] = bios_version[e.bios_versionZipf.Uint64()]
e.record[5] = operating_system[e.operating_systemZipf.Uint64()]
e.record[6] = system_ram[e.system_ramZipf.Uint64()]
e.record[7] = e.generateRandomInt(20, 0)
e.record[8] = e.generateRandomInt(20, 0)
e.record[9] = epsa_fail_code[e.epsa_fail_codeZipf.Uint64()]
e.record[10] = epsa_version[e.epsa_versionZipf.Uint64()]
e.record[11] = video_controller[e.video_controllerZipf.Uint64()]
e.record[12] = ddv_revision[e.ddv_revisionZipf.Uint64()]
e.record[13] = motherboard_eppid[e.motherboard_eppidZipf.Uint64()]
e.record[14] = e.generateRandomInt(30, 0)
e.record[15] = processor_information[e.processor_informationZipf.Uint64()]
e.cur++
return e.record, nil
}
// Schema implements idk.Source.
func (e *DellSource) Schema() []idk.Field {
return e.schema
}
func (e *DellSource) Seed(seed int64) {
e.rand.Seed(seed)
}
var _ Seedable = (*DellSource)(nil)
const charsetDell = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// returns a string with random characters from a predefined charset of a specified length
func (e *DellSource) StringWithCharset(length int, charset string) string {
b := make([]byte, length)
for i := range b {
b[i] = charsetDell[e.rand.Intn(len(charsetDell))]
}
return string(b)
}
//returns a string with random English alphabets of the specified length
func (e *DellSource) StringDell(length int) string {
return e.StringWithCharset(length, charsetDell)
}
//generates a random int between range max and min (inclusive)
func (e *DellSource) generateRandomInt(max int, min int) int {
return e.rand.Intn(max-min) + min
}
func (e *DellSource) Close() error {
return nil
}

124
idk/datagen/dwarranty.go Normal file
View file

@ -0,0 +1,124 @@
package datagen
import (
"fmt"
"io"
"math/rand"
"github.com/molecula/featurebase/v3/idk"
)
// Ensure DWarranty implements interface.
var _ Sourcer = (*DWarranty)(nil)
// DWarranty implements Sourcer.
type DWarranty struct{}
// NewDWarranty returns a new instance of DWarranty.
func NewDWarranty(cfg SourceGeneratorConfig) Sourcer {
return &DWarranty{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (d *DWarranty) Source(cfg SourceConfig) idk.Source {
min := int64(0)
src := &DWarrantySource{
totalGenerating: cfg.total,
WarrantySource: WarrantySource{
startFrom: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(19)),
schema: []idk.Field{
idk.IDField{NameVal: "id"},
idk.StringField{NameVal: "type"},
idk.IntField{NameVal: "start", Min: &min},
idk.IntField{NameVal: "last", Min: &min},
idk.IntField{NameVal: "cust_id", Min: &min},
idk.IntField{NameVal: "item_id", Min: &min},
idk.StringField{NameVal: "prod"},
},
},
}
src.typeZipf = rand.NewZipf(src.rand, 1.03, 5, uint64(len(types)-1))
src.prodZipf = rand.NewZipf(src.rand, 1.2, 8, uint64(len(prods)-1))
src.custZipf = rand.NewZipf(src.rand, 1.01, 4, 100000000)
src.record = make([]interface{}, len(src.schema))
src.record[2] = make([]string, 52*5)
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (d *DWarranty) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (d *DWarranty) DefaultEndAt() uint64 {
return 2147483648
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (d *DWarranty) Info() string {
return "TODO"
}
// Ensure DWarrantySource implements interface.
var _ idk.Source = (*DWarrantySource)(nil)
type DWarrantySource struct {
WarrantySource
totalGenerating uint64
prodZipf *rand.Zipf
}
func (s *DWarrantySource) Record() (idk.Record, error) {
if s.startFrom >= s.endAt {
return nil, io.EOF
}
s.record[0] = s.startFrom
s.record[1] = types[s.typeZipf.Uint64()]
startDay := (s.startFrom * uint64(daysSinceStart)) / s.totalGenerating
days := numDays(s.rand)
s.record[2] = startDay
s.record[3] = startDay + uint64(days)
s.record[4] = int64(s.custZipf.Uint64()) // 100M customers
s.record[5] = s.rand.Intn(1500000000) // 1.5B items
s.record[6] = prods[s.prodZipf.Uint64()]
s.startFrom++
return s.record, nil
}
func (s *DWarrantySource) Schema() []idk.Field {
return s.schema
}
var _ Seedable = (*DWarrantySource)(nil)
var types = make([]string, 100)
var prods = make([]string, 3200)
func init() {
var baseTypes = []string{"AZ-PQ-SUP1", "AZ-PQ-GOLDSUP", "BASICSUPPORT", "LM-NO-BASICLOWSUP", "QZ-PG-NONE", "CLIENTFGBG", "PHONE-BUNDLE", "AZ-PQ-PLATSUP", "AZ-PQ-PARTSUP", "OTHERSUP"}
for i := range types {
bt := baseTypes[i%len(baseTypes)]
types[i] = fmt.Sprintf("%s-%d", bt, i)
}
for i := range prods {
prods[i] = fmt.Sprintf("PRD-%d", i)
}
}
func (s *DWarrantySource) Close() error {
return nil
}

File diff suppressed because one or more lines are too long

282
idk/datagen/equipment.go Normal file
View file

@ -0,0 +1,282 @@
package datagen
import (
"errors"
"fmt"
"io"
"math/rand"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
)
// Ensure Equipment implements interface.
var _ Sourcer = (*Equipment)(nil)
// Equipment implements Sourcer.
type Equipment struct{}
// NewEquipment returns a new instance of Equipment.
func NewEquipment(cfg SourceGeneratorConfig) Sourcer {
return &Equipment{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (e *Equipment) Source(cfg SourceConfig) idk.Source {
src := &EquipmentSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(22)),
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.StringField{NameVal: "domain"}, // 1
idk.IntField{NameVal: "cost"}, // 2
idk.IntField{NameVal: "site_id"}, // 3
idk.DateIntField{NameVal: "date_acquired", Unit: idk.Day}, // 4
idk.DateIntField{NameVal: "last_maintenance", Unit: idk.Day}, // 5
idk.StringField{NameVal: "name"}, // 6
idk.StringField{NameVal: "type"}, // 7
idk.StringField{NameVal: "manufacturer"}, // 8
idk.StringField{NameVal: "model"}, // 9
},
mfgrZipfs: make(map[string]map[string]*rand.Zipf),
modelZipfs: make(map[string]map[string]map[string]*rand.Zipf),
}
src.dcZipf = rand.NewZipf(src.rand, 1.5, 6, 3000000)
// set up zipfs for all equipment manufacturers and models
//
// TODO: I believe the fact that we make rand calls during map
// iteration (where order is random) is making this datagen
// non-reproducible.
for dom, typeMap := range domains {
src.mfgrZipfs[dom] = make(map[string]*rand.Zipf)
src.modelZipfs[dom] = make(map[string]map[string]*rand.Zipf)
for typ, mfgrMap := range typeMap {
src.modelZipfs[dom][typ] = make(map[string]*rand.Zipf)
src.mfgrZipfs[dom][typ] = rand.NewZipf(src.rand, 1.001+src.rand.Float64(), float64(src.rand.Intn(7))+src.rand.Float64()+1, uint64(len(mfgrMap))-1)
for mfgr, models := range mfgrMap {
src.modelZipfs[dom][typ][mfgr] = rand.NewZipf(src.rand, 1.001+src.rand.Float64(), float64(src.rand.Intn(7))+src.rand.Float64()+1, uint64(len(models))-1)
}
}
}
src.record = make([]interface{}, len(src.schema))
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (e *Equipment) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (e *Equipment) DefaultEndAt() uint64 {
return 36000000 - 1
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (e *Equipment) Info() string {
return "Generates equipment and device data (manufacturer, cost, etc) - includes a foreign key for network_ts, power_ts, transaction_ts."
}
// Ensure EquipmentSource implements interface.
var _ idk.Source = (*EquipmentSource)(nil)
type EquipmentSource struct {
Log logger.Logger
rand *rand.Rand
mfgrZipfs map[string]map[string]*rand.Zipf
modelZipfs map[string]map[string]map[string]*rand.Zipf
dcZipf *rand.Zipf
schema []idk.Field
cur, endAt uint64
record record
}
func (s *EquipmentSource) Record() (idk.Record, error) {
if s.cur >= s.endAt {
return nil, io.EOF
}
s.record[0] = s.cur
switch {
case s.cur < 5000000:
s.fillRecord("Power", s.powerType())
case s.cur < 35000000:
s.fillRecord("Communications", s.communicationsType())
case s.cur < 36000000:
s.fillRecord("Transactions", s.transactionsType())
default:
return nil, errors.New("exhausted equipment data set")
}
s.cur++
return s.record, nil
}
func (s *EquipmentSource) fillRecord(domain, eqType string) {
s.record[1] = domain
// type
s.record[7] = eqType
// manufacturer
mfgrs := mfgrLists[domain][eqType]
mfgr := mfgrs[s.mfgrZipfs[domain][eqType].Uint64()]
s.record[8] = mfgr
// model
models := domains[domain][eqType][mfgr]
modelZipf, ok := s.modelZipfs[domain][eqType][mfgr]
if !ok {
s.Log.Printf("%s %s %s", domain, eqType, mfgr)
}
model := models[modelZipf.Uint64()]
s.record[9] = model
// cost
s.record[2] = costs[domain][eqType].Cost(s.rand)
// site id
s.record[3] = s.siteID(eqType)
// date acquired
s.record[4] = startDayInt + s.rand.Int63n(365*10-200)
// last maintenance
s.record[5] = s.record[4].(int64) + s.rand.Int63n(todayInt-s.record[4].(int64))
}
func (s *EquipmentSource) powerType() string {
num := s.rand.Intn(100)
switch {
case num < 75:
return "Breaker"
case num < 77:
return "Generator"
case num < 82:
return "Air Handler"
case num < 88:
return "Chiller"
default:
return "UPS"
}
}
func (s *EquipmentSource) siteID(typ string) int64 {
switch typ {
case "Breaker":
return s.rand.Int63n(4680000)
case "Generator":
return s.rand.Int63n(4680000)
case "Air Handler":
return s.rand.Int63n(4680000)
case "Chiller":
return s.rand.Int63n(4680000)
case "UPS":
return s.rand.Int63n(4680000)
case "Router":
num := s.rand.Int63n(1000 + 7000 + 3000000)
switch {
case num < 8000:
// MSC or Headend
return num
default:
// datacenter
return num + 1680000
}
case "Server":
num := s.rand.Intn(100)
// not a datacenter
if num == 0 {
// non-DC
return s.rand.Int63n(1688000)
} else {
// DC
return int64(s.dcZipf.Uint64()) + 1688000
}
case "BTS":
// Cell Site
return s.rand.Int63n(400000) + 488000
case "CPE":
// Retail
return s.rand.Int63n(400000) + 88000
case "POS":
// Retail
return s.rand.Int63n(400000) + 88000
default:
panic(fmt.Sprintf("unknown type %s", typ))
}
}
func (s *EquipmentSource) communicationsType() string {
// 30M communications eq from 5M to 35M
// 1M router, 14M Server, 13M CPE, 2M BTS
id := s.record[0].(uint64)
switch {
case id < 18000000:
return "CPE"
case id < 19000000:
return "Router"
case id < 33000000:
return "Server"
default:
return "BTS"
}
}
func (s *EquipmentSource) transactionsType() string { return "POS" }
func (s *EquipmentSource) Schema() []idk.Field {
return s.schema
}
func (s *EquipmentSource) Seed(seed int64) {
s.rand.Seed(seed)
}
var _ Seedable = (*EquipmentSource)(nil)
var mfgrLists = make(map[string]map[string][]string)
func init() {
for name, typeMap := range domains {
mfgrLists[name] = make(map[string][]string)
for typ, mfgrMap := range typeMap {
mfgrs := make([]string, 0)
for mfgr := range mfgrMap {
mfgrs = append(mfgrs, mfgr)
}
mfgrLists[name][typ] = mfgrs
}
}
}
type minMax struct {
min int64
max int64
}
func (m minMax) Cost(r *rand.Rand) int64 {
return r.Int63n(m.max-m.min) + m.min
}
func (s *EquipmentSource) Close() error {
return nil
}

100
idk/datagen/example.go Normal file
View file

@ -0,0 +1,100 @@
package datagen
import (
"fmt"
"io"
"github.com/molecula/featurebase/v3/idk"
)
// Ensure Example implements interface.
var _ Sourcer = (*Example)(nil)
// Example implements Sourcer, and returns a very basic
// data set. It can be used as an example for writing
// additional custom Sourcers.
type Example struct{}
// NewExample returns a new instance of Example.
func NewExample(cfg SourceGeneratorConfig) Sourcer {
return &Example{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
// NOTE: avro Name fields can only contain certain
// characters.
// See: https://avro.apache.org/docs/current/spec.html#names
func (e *Example) Source(cfg SourceConfig) idk.Source {
src := &ExampleSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.StringField{NameVal: "string"}, // 1
},
}
src.record = make([]interface{}, len(src.schema))
src.record[0] = uint64(0)
src.record[1] = make([]byte, 12)
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (e *Example) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (e *Example) DefaultEndAt() uint64 {
return 100
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (e *Example) Info() string {
return "generate data for a couple of fields to use as an example"
}
// Ensure ExampleSource implements interface.
var _ idk.Source = (*ExampleSource)(nil)
// ExampleSource is an instance of a source generated
// by the Sourcer implementation Example.
type ExampleSource struct {
cur uint64
endAt uint64
schema []idk.Field
record record
}
// Record implements idk.Source.
func (e *ExampleSource) Record() (idk.Record, error) {
if e.cur >= e.endAt {
return nil, io.EOF
}
// Increment the ID.
e.record[0] = e.cur
e.record[1] = []byte(fmt.Sprintf("foo-%d", e.cur))
e.cur++
return e.record, nil
}
// Schema implements idk.Source.
func (e *ExampleSource) Schema() []idk.Field {
return e.schema
}
func (e *ExampleSource) Close() error {
return nil
}

View file

@ -0,0 +1,65 @@
package gen
var Animals = []string{"Millipede", "Reindeer", "Otter", "Umbrellabird", "Bandicoot", "Macaroni Penguin", "Australian Cattle Dog", "Hercules Beetle", "Magpie", "Bernese Mountain Dog",
"Royal Penguin", "Manatee", "Black Russian Terrier", "Badger", "Indian Elephant", "Indochinese Tiger", "Centipede", "Bluetick Coonhound", "Humpback Whale", "Caiman Lizard",
"Grasshopper", "Tortoise", "Alpine Dachsbracke", "Quetzal", "Aldabra Giant Tortoise", "Electric Eel", "Vervet Monkey", "Mountain Lion", "Geoffroys Tamarin", "Collared Peccary",
"Doberman Pinscher", "Megalodon", "Insect", "Shiba Inu", "Tiger Shark", "Aye Aye", "Black Rhinoceros", "Gar", "Stick Insect", "Marine Toad",
"Grouse", "Javanese", "Water Dragon", "Whippet", "Giant Clam", "Caiman", "Fennec Fox", "Weasel", "Rat", "Golden Retriever Complete Pet Guide",
"Starfish", "Moth", "Horned Frog", "Water Vole", "King Crab", "Common Frog", "Neanderthal", "Fossa", "Frigatebird", "Spider Monkey",
"Appenzeller Dog", "Emperor Penguin", "Saint Bernard", "Eagle", "Platypus", "Monkfish", "Lizard", "Barracuda", "Wombat", "Pika",
"Yak", "Ibis", "Beetle", "Horse", "Arctic Fox", "Chihuahua", "African Wild Dog", "Bat", "Dogue De Bordeaux", "Akita",
"Mastiff", "Fox Terrier", "Thorny Devil", "Dachshund", "Zonkey", "Dormouse", "Guinea Pig", "Edible Frog", "Barnacle", "Asian Palm Civet",
"Irish WolfHound", "Giant Panda Bear", "Abyssinian", "Toucan", "Wild Boar", "Grizzly Bear", "Borneo Elephant", "Dusky Dolphin", "Gecko", "Golden Lion Tamarin",
"Deer", "Indri", "Stoat", "Skunk", "Sea Lion", "Turkey", "Octopus", "Pelican", "Meerkat", "Pig",
"Glass Lizard", "Fur Seal", "Pekingese", "Bottlenose Dolphin", "Fox", "Siamese", "Scorpion Fish", "Howler Monkey", "Anatolian Shepherd Dog", "Mountain Gorilla",
"Burrowing Frog", "African Bush Elephant", "Havanese", "Sumatran Elephant", "Glow Worm", "Saber-Toothed Tiger", "Japanese Chin", "Tuatara", "Estrela Mountain Dog", "Cow",
"Dwarf Crocodile", "Flounder", "Tetra", "Gorilla", "Bearded Collie", "Angelfish", "Gentoo Penguin", "Afghan Hound", "Porpoise", "Australian Kelpie Dog",
"Pygmy Marmoset", "Harpy Eagle", "Ostrich", "Woolly Mammoth", "Duck", "Binturong", "Sea Slug", "Sumatran Tiger", "Cassowary", "King Penguin",
"Bongo", "Basset Hound", "Siberian Husky", "Camel Spider", "Boxer Dog", "Cockroach", "Antelope", "Foxhound", "Beagle", "Impala",
"Snapping Turtle", "Grey Reef Shark", "Jellyfish", "Pheasant", "Cavalier King Charles Spaniel", "Monarch Butterfly", "Wrasse", "Drever", "Catfish", "Malayan Civet",
"Bearded Dragon", "Red Wolf", "Cuttlefish", "Chipmunk", "English Springer Spaniel", "Tiger Salamander", "Epagneul Pont Audemer", "Mongoose", "Somali", "Parrot",
"Burmese", "Greyhound", "Tiger", "Tree Frog", "Skate Fish", "Flamingo", "Bonobo", "Ibizan Hound", "Spadefoot Toad", "Japanese Macaque",
"Leopard Seal", "Leopard Tortoise", "Moose", "Chicken", "Avocet", "Australian Mist", "Little Penguin", "Quail", "Elephant Seal", "English Cocker Spaniel",
"Humboldt Penguin", "Bedlington Terrier", "Blue Whale", "Tibetan Mastiff", "Pink Fairy Armadillo", "Hedgehog", "Bavarian Mountain Hound", "Basking Shark", "Quoll", "West Highland Terrier",
"Polar Bear", "Egyptian Mau", "Fish", "Crocodile", "Vulture", "Arctic Wolf", "Javan Rhinoceros", "Capybara", "Baboon", "Gharial",
"Hawaiian Crow", "Piranha", "Australian Shepherd", "Fishing Cat", "Sponge", "Guppy", "Tasmanian Devil", "Chesapeake Bay Retriever", "Lobster", "Elephant",
"Labradoodle The Complete Guide For Owners", "Radiated Tortoise", "Dugong", "French Bulldog", "Slow Worm", "Bulldog", "Giant African Land Snail", "African Tree Toad", "Serval", "Nurse Shark",
"Kangaroo", "Jackal", "Neapolitan Mastiff", "Cockatoo", "Pond Skater", "Okapi", "Hyena", "Border Terrier", "Silver Dollar", "Dog",
"Coati", "Butterfly", "Moorhen", "Numbat", "Human", "King Cobra", "Green Bee-Eater", "Common Loon", "Cross River Gorilla", "Moray Eel",
"Bloodhound", "Spixs Macaw", "Sperm Whale", "Russian Blue", "Persian", "Chimpanzee", "Tiffany", "African Civet", "Koala", "Tapir",
"Minke Whale", "Clumber Spaniel", "Leopard", "Liger", "Red Panda", "Wolf Spider", "Mayfly", "Zebra", "Bombay", "Rattlesnake",
"African Forest Elephant", "Yorkshire Terrier", "Jack Russel", "Maltese", "Booby", "Bison", "Woodpecker", "Squid", "Uguisu", "Sumatran Orang-utan",
"Asian Giant Hornet", "Pink Dolphin", "German Pinscher", "Dalmatian", "Sun Bear", "Caracal", "Camel", "Vampire Bat", "Mole", "Dolphin",
"Nightingale", "German Shepherd Guide", "Coyote", "Mandrill", "Newfoundland", "Cairn Terrier", "Kingfisher", "Darwins Frog", "Mouse", "Aardvark",
"Greater Swiss Mountain Dog", "Pied Tamarin", "Monkey", "Raccoon", "Sea Turtle", "Sloth", "Airedale Terrier", "Alligator", "Orang-utan", "Wyoming Toad",
"Shrimp", "Stellers Sea Cow", "Cotton-top Tamarin", "Asiatic Black Bear", "Western Lowland Gorilla", "Alaskan Malamute", "Caterpillar", "Spiny Dogfish", "Bull Terrier", "Maned Wolf",
"Cocker Spaniel", "Buffalo", "Lemming", "Crane", "Tang", "Monitor Lizard", "Crab-Eating Macaque", "Kudu", "Dunker", "Border Collie",
"Bobcat", "Grey Mouse Lemur", "South China Tiger", "Bolognese Dog", "Kiwi", "Horseshoe Crab", "Highland Cattle", "Stingray", "Macaw", "Patas Monkey",
"Bornean Orang-utan", "Birman", "Emu", "Spectacled Bear", "Lionfish", "Golden Masked Owl", "Honey Badger", "Wildebeest", "Quokka", "Hippopotamus",
"Heron", "Cichlid", "Water Buffalo", "Chinook", "African Penguin", "Common Toad", "Termite", "Affenpinscher", "Leaf-Tailed Gecko", "Maine Coon",
"Lynx", "Bactrian Camel", "Water Spaniel", "Harrier", "Manta Ray", "Coral", "Dogo Argentino", "Gerbil", "Australian Terrier", "Masked Palm Civet",
"Snake", "Desert Tortoise", "Axolotl", "Greenland Dog", "Indian Palm Squirrel", "Fluke Fish (summer flounder)", "Woolly Monkey", "Ladybug", "Wolf", "Common Buzzard",
"African Palm Civet", "Cesky Fousek", "Mule", "Amur Leopard", "Norwegian Forest", "Flying Squirrel", "Eskimo Dog", "Monte Iberia Eleuth", "Lion", "Eastern Lowland Gorilla",
"Blobfish", "Chamois", "Arctic Hare", "Grey Seal", "English Shepherd", "Swan", "Sea Urchin", "Birds Of Paradise", "Butterfly Fish", "Blue Jay",
"Staffordshire Bull Terrier", "Siamese Fighting Fish", "White-Faced Capuchin", "Whale Shark", "Snail", "Pit Bull Terrier", "Squirrel Monkey", "Hoopoe", "Sea Dragon", "Seahorse",
"Great Dane", "Clown Fish", "Snowy Owl", "Anteater", "White Rhinoceros", "Boykin Spaniel", "Green Anole", "Pug", "Dhole", "Goose",
"Deutsche Bracke", "Banded Palm Civet", "Llama", "Sri Lankan Elephant", "Welsh Corgi: The Complete Pet Guide", "Tropicbird", "Rhinoceros", "Curly Coated Retriever", "Leopard Cat", "Black Widow Spider",
"Ragdoll", "Horn Shark", "Donkey", "Elephant Shrew", "Armadillo", "Keel Billed Toucan", "Purple Emperor", "Scorpion", "Chameleon", "Sea Otter",
"Giant Schnauzer", "Bear", "Honey Bee", "Zebu", "African Clawed Frog", "Blue Lacy Dog", "Pangolin", "Cat", "Collie", "Dodo",
"Brown Bear", "Akbash", "Tapanuli Orang-utan", "Puma", "Iguana", "Uakari", "Chinese Crested Dog", "Magellanic Penguin", "Hare", "Molly",
"Peacock", "Saola", "Squirrel", "Basenji Dog", "Bumblebee", "Ferret", "Staffordshire Terrier", "Ainu Dog", "Rottweiler", "Beaver",
"Sparrow", "Sea Squirt", "Labrador Retriever", "Pool Frog", "Bichon Frise", "Oyster", "Panther", "Hammerhead Shark", "Frog", "Ocelot",
"Coonhound", "Rabbit", "Adelie Penguin", "Olm", "Porcupine", "Hummingbird", "Wasp", "Tawny Owl", "Walrus", "X-Ray Tetra",
"Goat", "Komodo Dragon", "Fly", "Western Gorilla", "Golden-Crowned Flying Fox", "Stag Beetle", "Budgerigar", "Chinchilla", "Finnish Spitz", "Woodlouse",
"Bull Mastiff", "Jaguar", "River Turtle", "Wolverine", "Siberian Tiger", "Jerboa", "Crab", "Emperor Tamarin", "Kakapo", "Hermit Crab",
"Eskimo Dog", "North American Black Bear", "Galapagos Penguin", "Chow Chow", "Red-handed Tamarin", "Albatross", "Roseate Spoonbill", "Siberian", "Striped Rocket Frog", "Malayan Tiger",
"Boston Terrier: Complete Pet Guide", "Poodle", "Clouded Leopard", "Red Knee Tarantula", "Yellow-Eyed Penguin", "Zebra Shark", "Lemur", "Raccoon Dog", "Pointer", "Newt",
"Dragonfly", "Carolina Dog", "Xerus", "Crested Penguin", "Warthog", "Ermine", "Frilled Lizard", "Field Spaniel", "Himalayan", "Turkish Angora",
"Discus", "Echidna", "Markhor", "Pygmy Hippopotamus", "Fin Whale", "Canaan Dog", "Marsh Frog", "Gopher", "Bull Shark", "Salamander",
"Brazilian Terrier", "Balinese", "Asian Elephant", "Prawn", "Robin", "Entlebucher Mountain Dog", "Snowshoe", "Galapagos Tortoise", "Barn Owl", "Proboscis Monkey",
"Wallaby", "Gila Monster", "Indian Rhinoceros", "Bullfrog", "Norfolk Terrier", "Sumatran Rhinoceros", "Bengal Tiger", "Penguin", "Mongrel", "Cougar",
"Narwhal", "Old English Sheepdog", "Flat Coat Retriever", "Pere Davids Deer", "Eastern Gorilla", "Barb", "Golden Oriole", "Pademelon", "Sheep", "Indian Star Tortoise",
"Aurochs", "Possum", "Zorse", "Chinstrap Penguin", "Sucker Fish", "Great White Shark", "Gibbon", "Guinea Fowl", "Sand Lizard", "Earwig",
"Seal", "Irish Setter Complete Pet Guide", "Rock Hyrax", "Cheetah", "Puffin", "American Bulldog", "Pufferfish", "White Tiger", "Hamster", "Rockhopper Penguin",
"Shih Tzu", "Fire-Bellied Toad", "Cuscus", "Pike", "Poison Dart Frog", "Dingo", "Puss Moth", "Ant", "Tarsier", "Scimitar-horned Oryx",
"Long-Eared Owl", "Falcon", "Killer Whale", "Bird", "Giraffe", "Opossum"}

154
idk/datagen/gen/gen.go Normal file
View file

@ -0,0 +1,154 @@
package gen
import (
"math/rand"
)
type Gen struct {
seed int64
// cache of zipfs for various imax. All have s==1.1 and v==10
zipfs map[uint64]*rand.Zipf
R *rand.Rand
}
type Opt func(g *Gen)
// OptGenSeed is a functional option for providing a seed
// to Gen.
func OptGenSeed(s int64) Opt {
return func(g *Gen) {
g.seed = s
}
}
// New creates a new *Gen
func New(opts ...Opt) *Gen {
g := &Gen{
seed: 17,
zipfs: make(map[uint64]*rand.Zipf),
}
for _, opt := range opts {
opt(g)
}
g.R = rand.New(rand.NewSource(g.seed))
return g
}
var alphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
var alphaLower = "abcdefghijklmnopqrstuvwxyz"
func (g *Gen) AlphaUpper(bs []byte) {
for i := range bs {
bs[i] = alphaUpper[g.R.Intn(len(alphaUpper))]
}
}
func (g *Gen) AlphaLower(bs []byte) {
for i := range bs {
bs[i] = alphaLower[g.R.Intn(len(alphaLower))]
}
}
// StringSliceFromList repopulates input with a random number of items
// from list. If input does not have enough capacity it may return a
// new list. There may be repeat items, there may be 0 items, there
// will never be more items than the length of list.
func (g *Gen) StringSliceFromList(input []string, list []string) []string {
input = input[:0]
num := g.R.Intn(len(list) + 1)
for i := 0; i < num; i++ {
input = append(input, list[g.R.Intn(len(list))])
}
return input
}
// StringSliceFromListWeighted picks between 0 and upto items from list randomly, but zipfian weighted toward items earlier in list. It will try to re-use input, but may allocate a new list if needed.
func (g *Gen) StringSliceFromListWeighted(input []string, list []string, s, v float64) []string {
input = input[:0]
z := g.getZipfParams(uint64(len(list)-1), s, v)
num := z.Uint64()
for i := uint64(0); i < num; i++ {
input = append(input, list[z.Uint64()])
}
return input
}
// StringFromListWeighted returns a random string from the list
// weighted toward the items earlier in the list.
func (g *Gen) StringFromListWeighted(list []string) string {
z := g.getZipf(uint64(len(list) - 1))
return list[z.Uint64()]
}
// StringFromList returns a random string from the list
// each with uniform probability
func (g *Gen) StringFromList(list []string) string {
return list[g.R.Intn(len(list))]
}
// Set generates a random set of integers between min and max (inclusive).
// The mean cardinality of the set is specified by typicalCardinality.
func (g *Gen) Set(min uint64, max uint64, typicalCardinality uint64) map[uint64]struct{} {
// The expected value of a negative binomial distribution is E(X) = r/p.
// In this case we want E(x) = typicalCardinality.
// In this context, a success means that we stop producing elements, so r = 1.
// Therefore, p = 1/typicalCardinality.
p := 1 / float64(typicalCardinality)
// Create a random target cardinality.
var n uint64
for n < max-min && g.R.Float64() > p {
n++
}
set := map[uint64]struct{}{}
if n > (max-min)/2 {
// The target cardinality is a substantial portion of the entire range.
// Generating random values and checking would result in many collisions and raise the complexity.
// Generate a list of values, shuffle it, and select the first n entries.
list := make([]uint64, max-min)
for i := range list {
list[i] = uint64(i) + min
}
g.R.Shuffle(len(list), func(i, j int) {
list[j], list[i] = list[i], list[j]
})
list = list[:n]
for _, v := range list {
set[v] = struct{}{}
}
} else {
// The target cardinality is a small portion of the range.
// Randomly selecting values should be fairly fast since collisions should be somewhat infrequent.
for uint64(len(set)) < n {
// Generate another value for the set.
set[min+uint64(g.R.Int63n(int64((max-min)+1)))] = struct{}{}
}
}
return set
}
func (g *Gen) getZipf(imax uint64) *rand.Zipf {
if nz, ok := g.zipfs[imax]; !ok {
nz = rand.NewZipf(g.R, 1.1, 10, imax)
g.zipfs[imax] = nz
return nz
} else {
return nz
}
}
func (g *Gen) getZipfParams(imax uint64, s, v float64) *rand.Zipf {
if nz, ok := g.zipfs[imax]; !ok {
nz = rand.NewZipf(g.R, s, v, imax)
g.zipfs[imax] = nz
return nz
} else {
return nz
}
}

View file

@ -0,0 +1,87 @@
package gen_test
import (
"testing"
"github.com/molecula/featurebase/v3/idk/datagen/gen"
)
func TestGen(t *testing.T) {
g := gen.New()
byteSlice := make([]byte, 6)
// Test AlphaUpper
g.AlphaUpper(byteSlice)
if len(byteSlice) != 6 {
t.Errorf("AlphaUpper modified byte slice length: %s", byteSlice)
}
for i, b := range byteSlice {
if b < 65 || b > 90 {
t.Errorf("non uppercase char at position %d: %d", i, b)
}
}
// Test AlphaLower
g.AlphaLower(byteSlice)
if len(byteSlice) != 6 {
t.Errorf("AlphaLower modified byte slice length: %s", byteSlice)
}
for i, b := range byteSlice {
if b < 97 || b > 122 {
t.Errorf("non lowercase char at position %d: %d", i, b)
}
}
// Test StringSliceFromList
var input []string
list := []string{"a", "b", "c", "d", "e", "f", "g"}
minLen, maxLen := 8, -1
for i := 0; i < 100; i++ {
randList := g.StringSliceFromList(input, list)
if len(randList) < minLen {
minLen = len(randList)
}
if len(randList) > maxLen {
maxLen = len(randList)
}
for j, item := range randList {
if len(item) != 1 || item[0] < 97 || item[0] > 103 {
t.Errorf("Unexpected list item at %d: %s. List: %v", j, item, randList)
}
}
}
if minLen != 0 {
t.Errorf("Unexpected minimum length of %d", minLen)
}
if maxLen != 7 {
t.Errorf("Unexpected maximum length of %d", maxLen)
}
// TestStringFromListWeighted
seen := make(map[string]int)
for i := 0; i < 100; i++ {
s := g.StringFromListWeighted([]string{"blah", "bleh", "blue"})
seen[s]++
if s != "blah" && s != "bleh" && s != "blue" {
t.Errorf("Unexpected string %s", s)
}
}
if len(seen) != 3 {
t.Errorf("Should have seen 3 different strings but saw: %v", seen)
}
seen = make(map[string]int)
for i := 0; i < 100; i++ {
s := g.StringFromListWeighted([]string{"blah", "bleh", "blue", "bler"})
seen[s]++
if s != "blah" && s != "bleh" && s != "blue" && s != "bler" {
t.Errorf("Unexpected string %s", s)
}
}
if len(seen) != 4 {
t.Errorf("Should have seen 4 different strings but saw: %v", seen)
}
}

114
idk/datagen/hobbies.data.go Normal file
View file

@ -0,0 +1,114 @@
package datagen
var hobbies = []string{
"Lego building", "Watching movies", "Watch making", "Slacklining", "BMX", "Cricket",
"Sketching", "Satellite watching", "Volunteering", "Radio-controlled model playing",
"Stone collecting", "Picnicking", "Hydroponics", "Karate", "Roller skating", "Skateboarding",
"Element collecting", "Weaving", "Beach volleyball", "Archery", "Livestreaming", "Stone skipping",
"Trapshooting", "Filmmaking", "Diorama", "Makeup", "Rugby league football", "Community activism",
"Field hockey", "Backpacking", "Slot car", "Insect collecting", "VR Gaming", "Video making",
"Bowling", "Sled dog racing", "Skiing", "Web design", "Sand art", "Public speaking",
"Movie memorabilia collecting", "Gardening", "Wikipedia editing", "Croquet", "Mathematics",
"Rail transport modeling", "Darts", "Judo", "Equestrianism", "Figure Skating", "Scrapbooking",
"Airbrushing", "Photography", "Climbing", "Tourism", "Journaling", "Flower growing",
"Wood carving", "Fashion design", "Polo", "Slot car racing", "Reading", "Electronic games",
"Martial arts", "Bell ringing", " Air sports", "Skipping rope", "Bowling", "Caving",
"Leather crafting", "Construction", "Bus riding", "Flag football", "Anime", "Whittling",
"Aerospace", "Sun bathing", "Music", "Running", "Diving", "Plastic art", "Stamp collecting",
"Gymnastics", "Kabaddi", "Coin collecting", "Video editing", "Stripping", "Cribbage",
"Candy making", "Amateur geology", "Motor sports", "Sculpting", "Transit map collecting",
"Refinishing", "Surfing", "Swimming", "Skateboarding", "Knowledge/word games", "Tether car",
"Poi", "Manga", " Action figure", "Teaching", "Blacksmithing", "Fingerpainting", "Audiophile",
"Spreadsheets", "Scouting", "Frisbee", "Metal detecting", "Book collecting", "Radio-controlled model playing",
"Films", "Karaoke", "Wargaming", "Biology", "DJing", "Axe throwing", "Volleyball",
"Life Science", "Fossil hunting", "Beachcombing", "Sudoku", "Cross-stitch", "Ephemera collecting",
"Puzzles", "Hiking/backpacking", "Digital hoarding", "Horseshoes", "Amateur astronomy",
"Book discussion clubs", "Model building", "Ceramics", "Telling jokes", "Gardening",
"Renaissance fair", "Record collecting", "Collecting", "Taxidermy", "Flying", "Zumba",
" Archaeology", "Quidditch", "Playing musical instruments", "Tapestry", "Perfume",
"Philately", "Business", "Microbiology", "Rafting", "Postcrossing", "Whisky", "Botany",
"Badminton", "Chatting", "Board sports", "Groundhopping", "Inventing", "Paragliding",
"Shooting sport", "Esports", "Sport stacking", "Proverbs", "Marching band", "Feng shui decorating",
"Car tuning", "Sociology", "Writing music", "Robot combat", "Parkour", "Shogi", "Weightlifting",
"Fashion", "Safari", "Motorcycling", "Pool", "Meteorology", "Auto audiophilia", "Mushroom hunting/mycology",
"Radio-controlled model playing", "Miniature art", "Video game developing", "Medical science",
"Herp keeping", "Shoemaking", "Gongfu tea", "Dowsing", "Microscopy", "Welding", "Woodworking",
"Clothesmaking", "Fingerprint collecting", "Crossword puzzles", "Breadmaking", "Ice hockey",
"Dolls", "Curling", "Sailing", "Mazes (indoor/outdoor)", "Fishkeeping", "Ticket collecting",
"Flower arranging", "Nail art", "Couponing", "Skimboarding", "Fishing", "Figure skating",
"Herping", "Surfing", "Go", "Vintage clothing", "Shortwave listening", "Water sports",
"Darts", "Bonsai", "Lomography", "Crocheting", "Meditation", "Cornhole", "Railway journeys",
"Cardistry", "Book restoration", "Graffiti", "Decorating", "Yo-yoing", "Speedcubing",
"Lotology (lottery ticket collecting)", "Houseplant care", "Cryptography", "Quilling",
"Powerlifting", "Cheesemaking", "Table tennis", "Public transport riding", "Pet adoption & fostering",
"Magnet fishing", "Hooping", "Bridge", "Rubik's Cube", "Beekeeping", "Digital arts",
"Foreign language learning", "Race walking", "Fusilately (phonecard collecting)",
"Fishfarming", "Jigsaw puzzles", "Reviewing Gadgets", "Entrepreneurship", "Pickleball",
"Wine tasting", "Footbag", "Astronomy", "Stuffed toy collecting", "Roller derby",
"Astrology", "Furniture building", "Lapidary", "Iceboat racing", "High-power rocketry",
"Reiki", "Baking", "Automobilism", "Witchcraft", "Walking", "Aerial silk", "Gongoozling",
"Learning", "Cartophily (card collecting)", "Paintball", "Genealogy", "Do it yourself",
"Volleyball", "Science and technology studies", "Horsemanship", "Swimming", "Needlepoint",
"Fishkeeping", "Vintage cars", "Basketball", "Qigong", "Video game collecting", "Writing",
"Vacation", "Nordic skating", "Powerboat racing", "Baseball", "Candle making", "Whale watching",
"Knot tying", "Ice skating", "Debate", "Checkers (draughts)", "Board/tabletop games",
"Model engineering", "VR Gaming", "Palmistry", "Air hockey", "Pole dancing", "Modeling",
"Puppetry", "Memory training", "Sculling or rowing", "Seashell collecting", "Poetry",
"Role-playing games", "Flying model planes", "Tennis polo", "Gymnastics", "Metalworking",
"Scutelliphily", "Eating", "Pet sitting", "Fruit picking", "Farming", "Survivalism",
"Fly tying", "Wax sealing", "Sea glass collecting", "Antiquing", "Metal detecting",
"Guerrilla gardening", "Dance", "Birdwatching", "Skiing", "Jujitsu", "Hiking", "Model aircraft",
"Model United Nations", "Jukskei", "Leaves", "Drama", "Lacrosse", "LARPing", "Home improvement",
"Skydiving", "Snowmobiling", "Meteorology", "Fantasy sports", "Blogging", "Hobby horsing",
"Knife throwing", "English", "Soapmaking", "Talking", "Lace making", "Driving", "Engraving",
"Kung fu", "Laser tag", "Composting", "Sledding", "Croquet", "Railway studies", "Magic",
"Kite flying", "Acting", "Juggling", "Travel", "Glassblowing", "Baton twirling", "Boxing",
"Kart racing", "Comic book collecting", "Meditation", "Mineral collecting", "Dancing",
"Antiquities", "Ultimate frisbee", "Planning", "Pole dancing", "Snorkeling", "Zoo visiting",
"Animation", "Rock painting", "Exhibition drill", "Stamp collecting", "People-watching",
"Knife collecting", "Herbalism", "Knitting", "Karting", "Tennis", "Drink mixing",
"Kombucha brewing", "Chemistry", "Badminton", "Lock picking", "Letterboxing", "Storm chasing",
"Sports memorabilia", "Tai chi", "Calligraphy", "Weight training", "Pin (lapel)",
"Coffee roasting", "Unicycling", "Ghost hunting", "Archery", "Museum visiting", "Card games",
"Dog sport", "Herping", "Netball", "Video gaming", "Trade fair visiting", "Baseball",
"plush collecting", "Car fixing & building", "Tatebanko", "BASE jumping", "Gold prospecting",
"Animal fancy", "Jogging", "Gunsmithing", "Shooting", "Long-distance running", "Quizzes",
"Canoeing", "Aquascaping", "Practical jokes", "Tattooing", "Social studies", "Vehicle restoration",
"Cheerleading", "Proofreading and editing", "Fishing", "Squash", "Tarot", "Sewing",
"Birdwatching", "Cycling", "Button collecting", "Animation", "Art", "Giving advice",
"Handball", "Die-cast toy", "Jewelry making", "Deltiology (postcard collecting)",
"Brazilian jiu-jitsu", "Coloring", "Podcast hosting", "Couch surfing", "Reading",
"Compact discs", "Bullet journaling", "Hunting", "Australian rules football", "Origami",
"Tea bag collecting", "Webtooning", "Longboarding", "Auto detailing", "Hacking", "Kendama",
"Photography", "Pilates", "Snowboarding", "Pressed flower craft", "Conlanging", "Beatboxing",
"Amateur radio", "Freestyle football", "Mountaineering", "Rock tumbling", "Yoga",
"Bus spotting", "Tour skating", "Rock balancing", "Camping", "Sculling or rowing",
"Performance", "Djembe", "Entertaining", "Chess", "Cleaning", "Electronics", "Vinyl Records",
"Beauty pageants", "Auto racing", "Climbing", "Road biking", "Gingerbread house making",
"Distro Hopping", "Geocaching", "Snowshoeing", "Creative writing", "Taekwondo", "Radio-controlled car racing",
"Worldbuilding", "Car riding", "Stand-up comedy", "Flying disc", "Dog walking", "Phillumeny",
"Foraging", "Singing", "Barbershop Music", "Confectionery", "Amusement park visiting",
"Inline skating", "Knife making", "History", "Breakdancing", "Experimenting", "Color guard",
"Painting", "Soccer", "Backgammon", "City trip", "Marbles", "Renovating", "Speed skating",
"Handball", "Gaming", "Triathlon", "Mountain biking", "Machining", "Art collecting",
"Baton twirling", "Horseback riding", "Benchmarking", "Philately", "Tourism", "Wrestling",
"Disc golf", "Flower collecting and pressing", "Fitness", "Acroyoga", "Beer tasting",
"Video gaming", "Lacrosse", "Bodybuilding", "Thrifting", "Topiary", "3D printing",
"Crystals", "Orienteering", "Noodling", "Geocaching", "Orienteering", "Winemaking",
"Watching documentaries", "Pet", "Drawing", "Photography", "Airsoft", "Homebrewing",
"Aircraft spotting", "Mini Golf", "Storytelling", "Pickleball", "Shuffleboard", "Cooking",
"Rock climbing", "Vegetable farming", "Radio-controlled model playing", "Billiards",
"Association football", "Embroidery", "Waxing", "Physics", "Hobby tunneling", "Scuba diving",
"Kayaking", "Videography", "Tennis", "Slot car", "Table tennis", "Golfing", "Dog training",
"Craft", "Mahjong", "Cycling", "Thru-hiking", "Fencing", "Airsoft", "Humor", "Mycology",
"Rail transport modelling", "Sports science", "Table football", "Trainspotting", "Minimalism",
"Urban exploration", "Macrame", "Computer programming", "Horseback riding", "Cue sports",
"Magic", "Pyrography", "Ice skating", "Upcycling", "Shoes", "Power Nap", "Pen Spinning",
"Jumping rope", "Astronomy", "Pottery", "Martial arts", "Butterfly watching", "Hula hooping",
"Water polo", "Geography", "Chess", "Rugby", "Cosplaying", "Racquetball", "Shopping",
"Graphic design", "Binge-watching", "Kitesurfing", "Research", "Model racing", "Listening to podcasts",
"Radio-controlled model collecting", "Research", "Rapping", "Poker", "Rappelling",
"Watching television", "Listening to music", "Mechanics", "Philosophy", "Recipe creation",
"Quilting", "Fossicking", "Social media", "Word searches", "Massaging", "Dominoes",
"Longboarding", "Scuba Diving", "Dining", "Hardware", "Communication", "Ant-keeping",
"Canyoning", "Dandyism", "Psychology", "Softball", "Table tennis playing"}

148
idk/datagen/hughes.go Normal file
View file

@ -0,0 +1,148 @@
package datagen
import (
"io"
"math/rand"
"time"
"github.com/molecula/featurebase/v3/idk"
)
// Ensure Hughes implements interface.
var _ Sourcer = (*Hughes)(nil)
// Implements Hughes as a Sourcer.
type Hughes struct{}
// NewHughes returns a new instance of Hughes.
func NewHughes(cfg SourceGeneratorConfig) Sourcer {
return &Hughes{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
// NOTE: avro Name fields can only contain certain
// characters.
// See: https://avro.apache.org/docs/current/spec.html#names
func (e *Hughes) Source(cfg SourceConfig) idk.Source {
epoch := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
layout := "2006-01-02 15:00"
src := &HughesSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(int64(cfg.startFrom))),
epoch: epoch,
layout: layout,
schema: []idk.Field{
idk.IDField{NameVal: "id"}, //0
idk.StringField{NameVal: "sid"}, //1
idk.DateIntField{ //2
NameVal: "time_stamp",
Epoch: epoch,
Unit: idk.Hour,
Layout: layout,
},
idk.DecimalField{NameVal: "metered_wan_b_mb_usage", Scale: 2}, //3
idk.DateIntField{ //4
NameVal: "asap_billing_cycle_gmt_start_date_time",
Epoch: epoch,
Unit: idk.Hour,
Layout: layout,
},
idk.DecimalField{NameVal: "metered_vsat_wan_a_mb_usage", Scale: 2}, //5
},
}
src.record = make([]interface{}, len(src.schema))
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (e *Hughes) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (e *Hughes) DefaultEndAt() uint64 {
return 6000
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (e *Hughes) Info() string {
return "Generates data representative of Baker Hughes' oil field service operations."
}
// Ensure HughesSource implements interface.
var _ idk.Source = (*HughesSource)(nil)
// HughesSource is an instance of a source generated
// by the Sourcer implementation Hughes.
type HughesSource struct {
cur uint64
endAt uint64
epoch time.Time
layout string
rand *rand.Rand
bDate int
schema []idk.Field
record record
}
// Record implements idk.Source.
func (e *HughesSource) Record() (idk.Record, error) {
if e.cur >= e.endAt {
return nil, io.EOF
}
e.record[0] = e.cur
if e.cur%6000 == 0 { // generates 6000 hours (January 1st to September 6th (23:00)) of data for each new unique SID
e.record[1] = e.SIDHughes(24)
e.bDate = e.rand.Intn(27) + 1
e.record[4] = e.epoch.AddDate(0, -1, e.bDate).Format(e.layout)
}
e.record[2] = e.epoch.Add(time.Duration(e.cur%6000) * time.Hour).Format(e.layout)
e.record[3] = e.floatRand(10000, 0)
currDate := e.epoch.Add(time.Duration(e.cur%6000) * time.Hour)
if currDate.Day() == e.bDate && currDate.Hour() == 0 {
e.record[4] = currDate.Format(e.layout)
}
e.record[5] = e.floatRand(10000, 0)
e.cur++
return e.record, nil
}
// Schema implements idk.Source.
func (e *HughesSource) Schema() []idk.Field {
return e.schema
}
//returns a random float between a range
func (e *HughesSource) floatRand(max float64, min float64) float64 {
return min + e.rand.Float64()*(max-min)
}
const charsetHughes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// returns a string with random characters from a predefined charset of a specified length
func (e *HughesSource) StringWithCharset(length int, charset string) string {
b := make([]byte, length)
for i := range b {
b[i] = charsetHughes[e.rand.Intn(len(charsetHughes))]
}
return string(b)
}
//returns a string with random English alphabets of the specified length
func (e *HughesSource) SIDHughes(length int) string {
return e.StringWithCharset(length, charsetHughes)
}
func (e *HughesSource) Close() error {
return nil
}

131
idk/datagen/item.go Normal file
View file

@ -0,0 +1,131 @@
package datagen
import (
"io"
"math/rand"
"time"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
)
// Ensure Item implements interface.
var _ Sourcer = (*Item)(nil)
// Item implements Sourcer.
type Item struct{}
// NewItem returns a new instance of Item.
func NewItem(cfg SourceGeneratorConfig) Sourcer {
return &Item{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (i *Item) Source(cfg SourceConfig) idk.Source {
min := int64(0)
src := &ItemSource{
rand: rand.New(rand.NewSource(19)),
startFrom: cfg.startFrom,
endAt: cfg.endAt,
schema: []idk.Field{
idk.IDField{NameVal: "id"},
idk.IntField{NameVal: "cust_id"},
idk.DecimalField{NameVal: "cost", Scale: 2},
idk.IntField{NameVal: "ship_date", Min: &min},
idk.StringField{NameVal: "pli"},
idk.IDField{NameVal: "prod"},
},
}
src.pliZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(plis))-1)
src.prodZipf = rand.NewZipf(src.rand, 1.01, 4, 3200)
src.custZipf = rand.NewZipf(src.rand, 1.01, 4, 100000000)
src.record = make([]interface{}, len(src.schema))
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (i *Item) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (i *Item) DefaultEndAt() uint64 {
return 1543503872
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (i *Item) Info() string {
return "TODO"
}
// Ensure ItemSource implements interface.
var _ idk.Source = (*ItemSource)(nil)
type ItemSource struct {
Log logger.Logger
startFrom uint64
endAt uint64
rand *rand.Rand
pliZipf *rand.Zipf
prodZipf *rand.Zipf
custZipf *rand.Zipf
schema []idk.Field
record record
}
type ItemSourceOption func(s *ItemSource) error
func OptItemStartFrom(start uint64) ItemSourceOption {
return func(s *ItemSource) error {
s.startFrom = start
return nil
}
}
func OptItemEndAt(end uint64) ItemSourceOption {
return func(s *ItemSource) error {
s.endAt = end
return nil
}
}
var plis = []string{"laptop", "desktop", "server", "rack", "phone", "fan", "enclosure", "switch", "monitor", "pli1", "pli2", "pli3", "pli4", "pli5", "pli6", "pli7", "pli8", "pli9", "pli10", "pli11", "pli12", "pli13", "pli14", "pli15", "pli16", "pli17", "pli18", "pli19", "pli20", "pli21", "pli22", "pli23", "pli24", "pli25", "pli26", "pli27", "pli28", "pli29", "pli30", "pli31", "pli32", "pli33", "pli34", "pli35", "pli36", "pli37", "pli38", "pli39", "pli40", "pli41", "pli42"}
func (s *ItemSource) Record() (idk.Record, error) {
if s.startFrom >= s.endAt {
return nil, io.EOF
}
s.record[0] = s.startFrom
s.startFrom++
s.record[1] = int64(s.custZipf.Uint64()) // cust_id
s.record[2] = s.rand.Float64() * 10000 // max $10k // cost
s.record[3] = int64(time.Duration(s.rand.Intn(int(currentDur))) / (time.Hour * 24)) // random number of days since start ship_date
s.record[4] = plis[s.pliZipf.Uint64()]
s.record[5] = s.prodZipf.Uint64()
return s.record, nil
}
func (s *ItemSource) Schema() []idk.Field {
return s.schema
}
func (s *ItemSource) Seed(seed int64) {
s.rand.Seed(seed)
}
var _ Seedable = (*ItemSource)(nil)
func (s *ItemSource) Close() error {
return nil
}

View file

@ -0,0 +1,163 @@
package datagen
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"io"
"sort"
"strconv"
"time"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/datagen/gen"
)
// Ensure KitchenSinkKeyed implements interface.
var _ Sourcer = (*KitchenSinkKeyed)(nil)
// KitchenSinkKeyed implements Sourcer.
type KitchenSinkKeyed struct {
schema []idk.Field
}
// NewKitchenSinkKeyed returns a new instance of KitchenSinkKeyed.
func NewKitchenSinkKeyed(cfg SourceGeneratorConfig) Sourcer {
return &KitchenSinkKeyed{
schema: []idk.Field{
idk.StringField{NameVal: "pk"}, // 0
idk.StringArrayField{NameVal: "set", CacheConfig: &idk.CacheConfig{CacheType: "lru", CacheSize: 1}}, // 1
idk.IntField{NameVal: "int", Min: intptr(-9223372036854775807), Max: intptr(9223372036854775807)}, // 2
idk.BoolField{NameVal: "bool"}, // 3
idk.StringField{NameVal: "time", Quantum: "YMD"}, // 4
idk.StringField{NameVal: "mutex", Mutex: true, CacheConfig: &idk.CacheConfig{CacheType: "ranked", CacheSize: 500}}, // 5
idk.StringField{NameVal: "string"}, // 6
idk.DecimalField{NameVal: "decimal", Scale: 2}, // 7
},
}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (k *KitchenSinkKeyed) Source(cfg SourceConfig) idk.Source {
src := &KitchenSinkKeyedSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
schema: k.schema,
}
src.g = gen.New(gen.OptGenSeed(cfg.seed))
src.record = make([]interface{}, len(src.schema))
src.record[0] = make([]byte, sha256.Size*(8/4))
src.record[6] = make([]byte, 12)
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (k *KitchenSinkKeyed) PrimaryKeyFields() []string {
return []string{"pk"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (k *KitchenSinkKeyed) DefaultEndAt() uint64 {
return 20000
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (k *KitchenSinkKeyed) Info() string {
return "Generates data to test everything but the kitchen sink. With Keys."
}
// Ensure KitchenSinkKeyedSource implements interface.
var _ idk.Source = (*KitchenSinkKeyedSource)(nil)
type KitchenSinkKeyedSource struct {
g *gen.Gen
cur, endAt uint64
schema []idk.Field
record record
}
func NewKitchenSinkKeyedSource(start, end uint64) *KitchenSinkKeyedSource {
src := &KitchenSinkKeyedSource{
cur: start,
endAt: end,
schema: []idk.Field{
idk.StringField{NameVal: "pk"}, // 0
idk.StringArrayField{NameVal: "set", CacheConfig: &idk.CacheConfig{CacheType: "lru", CacheSize: 1}}, // 1
idk.IntField{NameVal: "int", Min: intptr(-9223372036854775807), Max: intptr(9223372036854775807)}, // 2
idk.BoolField{NameVal: "bool"}, // 3
idk.StringField{NameVal: "time", Quantum: "YMD"}, // 4
idk.StringField{NameVal: "mutex", Mutex: true, CacheConfig: &idk.CacheConfig{CacheType: "ranked", CacheSize: 500}}, // 5
idk.StringField{NameVal: "string"}, // 6
idk.DecimalField{NameVal: "decimal", Scale: 2}, // 7
},
}
src.record = make([]interface{}, len(src.schema))
src.record[0] = make([]byte, sha256.Size*(8/4))
src.record[6] = make([]byte, 12)
return src
}
func (k *KitchenSinkKeyedSource) Record() (idk.Record, error) {
if k.cur >= k.endAt {
return nil, io.EOF
}
// Generate a unique string key.
var nbytes [64 / 8]byte
binary.LittleEndian.PutUint64(nbytes[:], k.cur)
hash := sha256.Sum256(nbytes[:])
hex.Encode(k.record[0].([]byte), hash[:])
k.cur++
// Generate a random set field.
set := k.g.Set(100, 1000, 10)
vals := make([]string, 0, len(set))
for v := range set {
vals = append(vals, "v"+strconv.FormatUint(v, 10))
}
sort.Strings(vals)
k.record[1] = vals
// Generate a random int field.
k.record[2] = ((2 * k.g.R.Int63n(2)) - 1) * k.g.R.Int63n(9223372036854775807)
// Generate a random bool field.
k.record[3] = k.g.R.Intn(2) == 0
// Generate a random time field.
k.record[4] = startTime.Add(time.Duration(k.g.R.Int63n(int64(timeSpan))))
// Generate a random mutex value.
k.record[5] = strconv.Itoa(k.g.R.Intn(20))
// Generate a random string field.
k.g.AlphaUpper(k.record[6].([]byte))
// Generate a random floating point value.
k.record[7] = float64(k.g.R.Int63()) / 100
return k.record, nil
}
func (k *KitchenSinkKeyedSource) Schema() []idk.Field {
return k.schema
}
func (k *KitchenSinkKeyedSource) Close() error {
return nil
}

139
idk/datagen/kitchen-sink.go Normal file
View file

@ -0,0 +1,139 @@
package datagen
import (
"io"
"sort"
"strconv"
"time"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/datagen/gen"
"github.com/molecula/featurebase/v3/pql"
)
// Ensure KitchenSink implements interface.
var _ Sourcer = (*KitchenSink)(nil)
// KitchenSink implements Sourcer.
type KitchenSink struct {
schema []idk.Field
}
// NewKitchenSink returns a new instance of KitchenSink.
func NewKitchenSink(cfg SourceGeneratorConfig) Sourcer {
return &KitchenSink{
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.StringArrayField{NameVal: "set", CacheConfig: &idk.CacheConfig{CacheType: "lru", CacheSize: 1}}, // 1
idk.IntField{NameVal: "int", Min: intptr(-9223372036854775807), Max: intptr(9223372036854775807)}, // 2
idk.BoolField{NameVal: "bool"}, // 3
idk.StringField{NameVal: "time", Quantum: "YMD"}, // 4
idk.StringField{NameVal: "mutex", Mutex: true, CacheConfig: &idk.CacheConfig{CacheType: "ranked", CacheSize: 500}}, // 5
idk.StringField{NameVal: "string"}, // 6
idk.DecimalField{NameVal: "decimal", Scale: 2}, // 7
},
}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (k *KitchenSink) Source(cfg SourceConfig) idk.Source {
src := &KitchenSinkSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
schema: k.schema,
}
src.g = gen.New(gen.OptGenSeed(cfg.seed))
src.record = make([]interface{}, len(src.schema))
src.record[0] = uint64(0)
src.record[6] = make([]byte, 12)
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (k *KitchenSink) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (k *KitchenSink) DefaultEndAt() uint64 {
return 20000
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (k *KitchenSink) Info() string {
return "Generates data to test everything but the kitchen sink."
}
// Ensure KitchenSinkSource implements interface.
var _ idk.Source = (*KitchenSinkSource)(nil)
// KitchenSinkSource is a data generator which generates
// data for all Pilosa field types.
type KitchenSinkSource struct {
g *gen.Gen
cur, endAt uint64
schema []idk.Field
record record
}
var endTime = time.Date(2020, time.May, 4, 12, 2, 28, 0, time.UTC)
var startTime = endTime.Add(-5 * 365 * 24 * time.Hour)
var timeSpan = endTime.Sub(startTime)
func (k *KitchenSinkSource) Record() (idk.Record, error) {
if k.cur >= k.endAt {
return nil, io.EOF
}
// Increment the ID.
k.record[0] = k.cur
k.cur++
// Generate a random set field.
set := k.g.Set(100, 1000, 10)
vals := make([]string, 0, len(set))
for v := range set {
vals = append(vals, "v"+strconv.FormatUint(v, 10))
}
sort.Strings(vals)
k.record[1] = vals
// Generate a random int field.
k.record[2] = ((2 * k.g.R.Int63n(2)) - 1) * k.g.R.Int63n(9223372036854775807)
// Generate a random bool field.
k.record[3] = k.g.R.Intn(2) == 0
// Generate a random time field.
k.record[4] = startTime.Add(time.Duration(k.g.R.Int63n(int64(timeSpan))))
// Generate a random mutex value.
k.record[5] = strconv.Itoa(k.g.R.Intn(20))
// Generate a random string field.
k.g.AlphaUpper(k.record[6].([]byte))
// Generate a random floating point value.
k.record[7] = pql.NewDecimal(k.g.R.Int63(), 2)
return k.record, nil
}
func (k *KitchenSinkSource) Schema() []idk.Field {
return k.schema
}
func (k *KitchenSinkSource) Close() error {
return nil
}

104535
idk/datagen/locations.data.go Normal file

File diff suppressed because it is too large Load diff

721
idk/datagen/merck.go Normal file
View file

@ -0,0 +1,721 @@
package datagen
import (
"io"
"math"
"math/rand"
"strconv"
"time"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/datagen/gen"
)
// Ensure Merck implements interface.
var _ Sourcer = (*Merck)(nil)
// Merck implements Sourcer, and returns a very basic
// data set. It can be used as an Merck for writing
// additional custom Sourcers.
type Merck struct{}
// NewMerck returns a new instance of Merck.
func NewMerck(cfg SourceGeneratorConfig) Sourcer {
return &Merck{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (e *Merck) Source(cfg SourceConfig) idk.Source {
src := &MerckSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(19)),
schema: []idk.Field{
idk.IDField{NameVal: "id"}, //0
idk.StringField{NameVal: "image_location"}, //1
idk.StringField{NameVal: "image_tag"}, //2
idk.StringField{NameVal: "algorithm_name"}, //3
idk.StringField{NameVal: "start_time"}, //4
idk.StringField{NameVal: "analysis_region"}, //5
idk.IntField{NameVal: "total_cells", Min: intptr(1000), Max: intptr(1000000)}, //6
idk.IntField{NameVal: "dye_1_positive_cells", Min: intptr(0), Max: intptr(1000000)}, //7
idk.IntField{NameVal: "dye_2_positive_cells", Min: intptr(0), Max: intptr(1000000)}, //8
idk.IntField{NameVal: "dye_3_positive_cells", Min: intptr(0), Max: intptr(1000000)}, //9
idk.IntField{NameVal: "dye_4_positive_cells", Min: intptr(0), Max: intptr(1000000)}, //10
idk.IntField{NameVal: "dye_5_positive_cells", Min: intptr(0), Max: intptr(1000000)}, //11
idk.IntField{NameVal: "dye_6_positive_cells", Min: intptr(0), Max: intptr(1000000)}, //12
idk.IntField{NameVal: "dye_7_positive_cells", Min: intptr(0), Max: intptr(1000000)}, //13
idk.IntField{NameVal: "dye_8_positive_cells", Min: intptr(0), Max: intptr(1000000)}, //14
idk.IntField{NameVal: "dye_1_positive_nuclei_cells", Min: intptr(0), Max: intptr(1000000)}, //15
idk.IntField{NameVal: "dye_2_positive_nuclei_cells", Min: intptr(0), Max: intptr(1000000)}, //16
idk.IntField{NameVal: "dye_3_positive_nuclei_cells", Min: intptr(0), Max: intptr(1000000)}, //17
idk.IntField{NameVal: "dye_4_positive_nuclei_cells", Min: intptr(0), Max: intptr(1000000)}, //18
idk.IntField{NameVal: "dye_5_positive_nuclei_cells", Min: intptr(0), Max: intptr(1000000)}, //19
idk.IntField{NameVal: "dye_6_positive_nuclei_cells", Min: intptr(0), Max: intptr(1000000)}, //20
idk.IntField{NameVal: "dye_7_positive_nuclei_cells", Min: intptr(0), Max: intptr(1000000)}, //21
idk.IntField{NameVal: "dye_8_positive_nuclei_cells", Min: intptr(0), Max: intptr(1000000)}, //22
idk.IntField{NameVal: "negative_nuclei_cells", Min: intptr(0), Max: intptr(1000000)}, //23
idk.IntField{NameVal: "dye_1_positive_cytoplasm_cells", Min: intptr(0), Max: intptr(1000000)}, //24
idk.IntField{NameVal: "dye_2_positive_cytoplasm_cells", Min: intptr(0), Max: intptr(1000000)}, //25
idk.IntField{NameVal: "dye_3_positive_cytoplasm_cells", Min: intptr(0), Max: intptr(1000000)}, //26
idk.IntField{NameVal: "dye_4_positive_cytoplasm_cells", Min: intptr(0), Max: intptr(1000000)}, //27
idk.IntField{NameVal: "dye_5_positive_cytoplasm_cells", Min: intptr(0), Max: intptr(1000000)}, //28
idk.IntField{NameVal: "dye_6_positive_cytoplasm_cells", Min: intptr(0), Max: intptr(1000000)}, //29
idk.IntField{NameVal: "dye_7_positive_cytoplasm_cells", Min: intptr(0), Max: intptr(1000000)}, //30
idk.IntField{NameVal: "dye_8_positive_cytoplasm_cells", Min: intptr(0), Max: intptr(1000000)}, //31
idk.IntField{NameVal: "negative_cytoplasm_cells", Min: intptr(0), Max: intptr(1000000)}, //32
idk.IntField{NameVal: "dye_1_positive_membrane_cells", Min: intptr(0), Max: intptr(1000000)}, //33
idk.IntField{NameVal: "dye_2_positive_membrane_cells", Min: intptr(0), Max: intptr(1000000)}, //34
idk.IntField{NameVal: "dye_3_positive_membrane_cells", Min: intptr(0), Max: intptr(1000000)}, //35
idk.IntField{NameVal: "dye_4_positive_membrane_cells", Min: intptr(0), Max: intptr(1000000)}, //36
idk.IntField{NameVal: "dye_5_positive_membrane_cells", Min: intptr(0), Max: intptr(1000000)}, //37
idk.IntField{NameVal: "dye_6_positive_membrane_cells", Min: intptr(0), Max: intptr(1000000)}, //38
idk.IntField{NameVal: "dye_7_positive_membrane_cells", Min: intptr(0), Max: intptr(1000000)}, //39
idk.IntField{NameVal: "dye_8_positive_membrane_cells", Min: intptr(0), Max: intptr(1000000)}, //40
idk.IntField{NameVal: "negative_membrane_cells", Min: intptr(0), Max: intptr(1000000)}, //41
idk.DecimalField{NameVal: "dye_1_per_positive_cells", Scale: 6}, //42
idk.DecimalField{NameVal: "dye_2_per_positive_cells", Scale: 6}, //43
idk.DecimalField{NameVal: "dye_3_per_positive_cells", Scale: 6}, //44
idk.DecimalField{NameVal: "dye_4_per_positive_cells", Scale: 6}, //45
idk.DecimalField{NameVal: "dye_5_per_positive_cells", Scale: 6}, //46
idk.DecimalField{NameVal: "dye_6_per_positive_cells", Scale: 6}, //47
idk.DecimalField{NameVal: "dye_7_per_positive_cells", Scale: 6}, //48
idk.DecimalField{NameVal: "dye_8_per_positive_cells", Scale: 6}, //49
idk.DecimalField{NameVal: "dye_1_per_positive_nuclei_cells", Scale: 6}, //50
idk.DecimalField{NameVal: "dye_2_per_positive_nuclei_cells", Scale: 6}, //51
idk.DecimalField{NameVal: "dye_3_per_positive_nuclei_cells", Scale: 6}, //52
idk.DecimalField{NameVal: "dye_4_per_positive_nuclei_cells", Scale: 6}, //53
idk.DecimalField{NameVal: "dye_5_per_positive_nuclei_cells", Scale: 6}, //54
idk.DecimalField{NameVal: "dye_6_per_positive_nuclei_cells", Scale: 6}, //55
idk.DecimalField{NameVal: "dye_7_per_positive_nuclei_cells", Scale: 6}, //56
idk.DecimalField{NameVal: "dye_8_per_positive_nuclei_cells", Scale: 6}, //57
idk.DecimalField{NameVal: "dye_1_per_positive_cytoplasm_cells", Scale: 6}, //58
idk.DecimalField{NameVal: "dye_2_per_positive_cytoplasm_cells", Scale: 6}, //59
idk.DecimalField{NameVal: "dye_3_per_positive_cytoplasm_cells", Scale: 6}, //60
idk.DecimalField{NameVal: "dye_4_per_positive_cytoplasm_cells", Scale: 6}, //61
idk.DecimalField{NameVal: "dye_5_per_positive_cytoplasm_cells", Scale: 6}, //62
idk.DecimalField{NameVal: "dye_6_per_positive_cytoplasm_cells", Scale: 6}, //63
idk.DecimalField{NameVal: "dye_7_per_positive_cytoplasm_cells", Scale: 6}, //64
idk.DecimalField{NameVal: "dye_8_per_positive_cytoplasm_cells", Scale: 6}, //65
idk.DecimalField{NameVal: "dye_1_per_positive_membrane_cells", Scale: 6}, //66
idk.DecimalField{NameVal: "dye_2_per_positive_membrane_cells", Scale: 6}, //67
idk.DecimalField{NameVal: "dye_3_per_positive_membrane_cells", Scale: 6}, //68
idk.DecimalField{NameVal: "dye_4_per_positive_membrane_cells", Scale: 6}, //69
idk.DecimalField{NameVal: "dye_5_per_positive_membrane_cells", Scale: 6}, //70
idk.DecimalField{NameVal: "dye_6_per_positive_membrane_cells", Scale: 6}, //71
idk.DecimalField{NameVal: "dye_7_per_positive_membrane_cells", Scale: 6}, //72
idk.DecimalField{NameVal: "dye_8_per_positive_membrane_cells", Scale: 6}, //73
idk.DecimalField{NameVal: "dye_1_positive_nuclei_avg_intensity", Scale: 6}, //74
idk.DecimalField{NameVal: "dye_1_positive_cytoplasm_avg_intensity", Scale: 6}, //75
idk.DecimalField{NameVal: "dye_1_positive_membrane_avg_intensity", Scale: 6}, //76
idk.DecimalField{NameVal: "dye_2_positive_nuclei_avg_intensity", Scale: 6}, //77
idk.DecimalField{NameVal: "dye_2_positive_cytoplasm_avg_intensity", Scale: 6}, //78
idk.DecimalField{NameVal: "dye_2_positive_membrane_avg_intensity", Scale: 6}, //79
idk.DecimalField{NameVal: "dye_3_positive_nuclei_avg_intensity", Scale: 6}, //80
idk.DecimalField{NameVal: "dye_3_positive_cytoplasm_avg_intensity", Scale: 6}, //81
idk.DecimalField{NameVal: "dye_3_positive_membrane_avg_intensity", Scale: 6}, //82
idk.DecimalField{NameVal: "dye_4_positive_nuclei_avg_intensity", Scale: 6}, //83
idk.DecimalField{NameVal: "dye_4_positive_cytoplasm_avg_intensity", Scale: 6}, //84
idk.DecimalField{NameVal: "dye_4_positive_membrane_avg_intensity", Scale: 6}, //85
idk.DecimalField{NameVal: "dye_5_positive_nuclei_avg_intensity", Scale: 6}, //86
idk.DecimalField{NameVal: "dye_5_positive_cytoplasm_avg_intensity", Scale: 6}, //87
idk.DecimalField{NameVal: "dye_5_positive_membrane_avg_intensity", Scale: 6}, //88
idk.DecimalField{NameVal: "dye_6_positive_nuclei_avg_intensity", Scale: 6}, //89
idk.DecimalField{NameVal: "dye_6_positive_cytoplasm_avg_intensity", Scale: 6}, //90
idk.DecimalField{NameVal: "dye_6_positive_membrane_avg_intensity", Scale: 6}, //91
idk.DecimalField{NameVal: "dye_7_positive_nuclei_avg_intensity", Scale: 6}, //92
idk.DecimalField{NameVal: "dye_7_positive_cytoplasm_avg_intensity", Scale: 6}, //93
idk.DecimalField{NameVal: "dye_7_positive_membrane_avg_intensity", Scale: 6}, //94
idk.DecimalField{NameVal: "dye_8_positive_nuclei_avg_intensity", Scale: 6}, //95
idk.DecimalField{NameVal: "dye_8_positive_cytoplasm_avg_intensity", Scale: 6}, //96
idk.DecimalField{NameVal: "dye_8_positive_membrane_avg_intensity", Scale: 6}, //97
idk.DecimalField{NameVal: "avg_nucleus_area_um2", Scale: 6}, //98
idk.DecimalField{NameVal: "avg_cytoplasm_area_um2", Scale: 6}, //99
idk.DecimalField{NameVal: "avg_cell_area_um2", Scale: 6}, //100
idk.IntField{NameVal: "area_analyzed_um2"}, //101
idk.IntField{NameVal: "image_zoom"}, //102
idk.BoolField{NameVal: "classifier"}, //103
idk.StringField{NameVal: "class_list"}, //104
idk.BoolField{NameVal: "classify_registered"}, //105
idk.StringField{NameVal: "classifier_output_type"}, //106
idk.StringField{NameVal: "dye_1"}, //107
idk.BoolField{NameVal: "dye_1_membrane_segmentation"}, //108
idk.DecimalField{NameVal: "dye_1_nucleus_weight", Scale: 3}, //109
idk.DecimalField{NameVal: "dye_1_nucleus_positive_threshold", Scale: 3}, //110
idk.DecimalField{NameVal: "dye_1_cytoplasm_positive_threshold", Scale: 3}, //111
idk.DecimalField{NameVal: "dye_1_membrane_positive_threshold", Scale: 3}, //112
idk.StringField{NameVal: "dye_1_nucleus_mask"}, //113
idk.StringField{NameVal: "dye_1_cytoplasm_mask"}, //114
idk.StringField{NameVal: "dye_1_membrane_mask"}, //115
idk.StringField{NameVal: "dye_2"}, //116
idk.BoolField{NameVal: "dye_2_membrane_segmentation"}, //117
idk.DecimalField{NameVal: "dye_2_nucleus_weight", Scale: 3}, //118
idk.DecimalField{NameVal: "dye_2_nucleus_positive_threshold", Scale: 3}, //119
idk.DecimalField{NameVal: "dye_2_cytoplasm_positive_threshold", Scale: 3}, //120
idk.DecimalField{NameVal: "dye_2_membrane_positive_threshold", Scale: 3}, //121
idk.StringField{NameVal: "dye_2_nucleus_mask"}, //122
idk.StringField{NameVal: "dye_2_cytoplasm_mask"}, //123
idk.StringField{NameVal: "dye_2_membrane_mask"}, //124
idk.StringField{NameVal: "dye_3"}, //125
idk.BoolField{NameVal: "dye_3_membrane_segmentation"}, //126
idk.DecimalField{NameVal: "dye_3_nucleus_weight", Scale: 3}, //127
idk.DecimalField{NameVal: "dye_3_nucleus_positive_threshold", Scale: 3}, //128
idk.DecimalField{NameVal: "dye_3_cytoplasm_positive_threshold", Scale: 3}, //129
idk.DecimalField{NameVal: "dye_3_membrane_positive_threshold", Scale: 3}, //130
idk.StringField{NameVal: "dye_3_nucleus_mask"}, //131
idk.StringField{NameVal: "dye_3_cytoplasm_mask"}, //132
idk.StringField{NameVal: "dye_3_membrane_mask"}, //133
idk.StringField{NameVal: "dye_4"}, //134
idk.BoolField{NameVal: "dye_4_membrane_segmentation"}, //135
idk.DecimalField{NameVal: "dye_4_nucleus_weight", Scale: 3}, //136
idk.DecimalField{NameVal: "dye_4_nucleus_positive_threshold", Scale: 3}, //137
idk.DecimalField{NameVal: "dye_4_cytoplasm_positive_threshold", Scale: 3}, //138
idk.DecimalField{NameVal: "dye_4_membrane_positive_threshold", Scale: 3}, //139
idk.StringField{NameVal: "dye_4_nucleus_mask"}, //140
idk.StringField{NameVal: "dye_4_cytoplasm_mask"}, //141
idk.StringField{NameVal: "dye_4_membrane_mask"}, //142
idk.StringField{NameVal: "dye_5"}, //143
idk.BoolField{NameVal: "dye_5_membrane_segmentation"}, //144
idk.DecimalField{NameVal: "dye_5_nucleus_weight", Scale: 3}, //145
idk.DecimalField{NameVal: "dye_5_nucleus_positive_threshold", Scale: 3}, //146
idk.DecimalField{NameVal: "dye_5_cytoplasm_positive_threshold", Scale: 3}, //147
idk.DecimalField{NameVal: "dye_5_membrane_positive_threshold", Scale: 3}, //148
idk.StringField{NameVal: "dye_5_nucleus_mask"}, //149
idk.StringField{NameVal: "dye_5_cytoplasm_mask"}, //150
idk.StringField{NameVal: "dye_5_membrane_mask"}, //151
idk.StringField{NameVal: "dye_6"}, //152
idk.BoolField{NameVal: "dye_6_membrane_segmentation"}, //153
idk.DecimalField{NameVal: "dye_6_nucleus_weight", Scale: 3}, //154
idk.DecimalField{NameVal: "dye_6_nucleus_positive_threshold", Scale: 3}, //155
idk.DecimalField{NameVal: "dye_6_cytoplasm_positive_threshold", Scale: 3}, //156
idk.DecimalField{NameVal: "dye_6_membrane_positive_threshold", Scale: 3}, //157
idk.StringField{NameVal: "dye_6_nucleus_mask"}, //158
idk.StringField{NameVal: "dye_6_cytoplasm_mask"}, //159
idk.StringField{NameVal: "dye_6_membrane_mask"}, //160
idk.StringField{NameVal: "dye_7"}, //161
idk.BoolField{NameVal: "dye_7_membrane_segmentation"}, //162
idk.DecimalField{NameVal: "dye_7_nucleus_weight", Scale: 3}, //163
idk.DecimalField{NameVal: "dye_7_nucleus_positive_threshold", Scale: 3}, //164
idk.DecimalField{NameVal: "dye_7_cytoplasm_positive_threshold", Scale: 3}, //165
idk.DecimalField{NameVal: "dye_7_membrane_positive_threshold", Scale: 3}, //166
idk.StringField{NameVal: "dye_7_nucleus_mask"}, //167
idk.StringField{NameVal: "dye_7_cytoplasm_mask"}, //168
idk.StringField{NameVal: "dye_7_membrane_mask"}, //169
idk.StringField{NameVal: "dye_8"}, //170
idk.BoolField{NameVal: "dye_8_membrane_segmentation"}, //171
idk.DecimalField{NameVal: "dye_8_nucleus_weight", Scale: 3}, //172
idk.DecimalField{NameVal: "dye_8_nucleus_positive_threshold", Scale: 3}, //173
idk.DecimalField{NameVal: "dye_8_cytoplasm_positive_threshold", Scale: 3}, //174
idk.DecimalField{NameVal: "dye_8_membrane_positive_threshold", Scale: 3}, //175
idk.StringField{NameVal: "dye_8_nucleus_mask"}, //176
idk.StringField{NameVal: "dye_8_cytoplasm_mask"}, //177
idk.StringField{NameVal: "dye_8_membrane_mask"}, //178
idk.DecimalField{NameVal: "nuclear_contrast_threshold", Scale: 3}, //179
idk.DecimalField{NameVal: "minimum_nuclear_intensity", Scale: 3}, //180
idk.DecimalField{NameVal: "nuclear_segmentation_aggressiveness", Scale: 3}, //181
idk.DecimalField{NameVal: "fill_nuclear_holes", Scale: 3}, //182
idk.DecimalField{NameVal: "minimum_nuclear_size", Scale: 3}, //183
idk.DecimalField{NameVal: "maximum_nuclear_size", Scale: 3}, //184
idk.DecimalField{NameVal: "minimum_nuclear_roundness", Scale: 3}, //185
idk.DecimalField{NameVal: "maximum_cytoplasm_radius", Scale: 3}, //186
idk.DecimalField{NameVal: "membrane_segmentation_agressiveness", Scale: 3}, //187
idk.DecimalField{NameVal: "minimum_cell_size", Scale: 3}, //188
idk.DecimalField{NameVal: "maximum_cell_size", Scale: 3}, //189
idk.StringField{NameVal: "output_image"}, //190
idk.DecimalField{NameVal: "store_object_cell_data"}, //191
idk.DecimalField{NameVal: "nuclear_size_minimum", Scale: 3}, //192
idk.DecimalField{NameVal: "nuclear_size_maximum", Scale: 3}, //193
idk.DecimalField{NameVal: "cell_size_minimum", Scale: 3}, //194
idk.DecimalField{NameVal: "cell_size_maximum", Scale: 3}, //195
idk.IDField{NameVal: "subject_id"}, //196
idk.StringField{NameVal: "su_species"}, //197
idk.StringField{NameVal: "su_sex"}, //198
idk.StringField{NameVal: "su_date_of_birth"}, //199
idk.IDField{NameVal: "case_id"}, //200
idk.IntField{NameVal: "c_age"}, //201
idk.IntField{NameVal: "c_weight"}, //202
idk.IDArrayField{NameVal: "procedure_id"}, //203
idk.StringArrayField{NameVal: "p_name"}, //204
idk.StringArrayField{NameVal: "p_description"}, //205
idk.IntField{NameVal: "p_ontology_code"}, //206
idk.IDField{NameVal: "diagnosis_id"}, //207
idk.StringField{NameVal: "dx_comments"}, //208
idk.IDField{NameVal: "disease_id"}, //209
idk.StringField{NameVal: "dz_disease_name"}, //210
idk.StringField{NameVal: "dz_etiology"}, //211
idk.StringField{NameVal: "dz_morphology"}, //212
idk.IntField{NameVal: "dz_ontology_code"}, //213
idk.IDField{NameVal: "specimen_id"}, //214
idk.StringField{NameVal: "sp_tissue_type"}, //215
idk.IntField{NameVal: "sp_sample_volume"}, //216
idk.StringField{NameVal: "sp_sample_location"}, //217
},
}
src.Z = gen.New(gen.OptGenSeed(cfg.seed))
src.algZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(alg))-1)
src.maskZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(mask))-1)
src.analysisregionZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(analysisregion))-1)
src.classifierTypeZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(classifierType))-1)
src.classListZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(classList))-1)
src.outputTypeZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(outputType))-1)
src.dyeZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(dye))-1)
src.outputImageZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(outputImage))-1)
src.speciesZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(species))-1)
src.sexZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(sex))-1)
src.commentsZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(comments))-1)
src.locationTissueZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(locationTissue))-1)
src.tissuetypeZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(tissuetype))-1)
src.etiologyZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(etiology))-1)
src.morphologyZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(morphology))-1)
src.procedures_descZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(procedures_desc))-1)
src.diseaseZipf = rand.NewZipf(src.rand, 1.01, 4, uint64(len(disease))-1)
src.record = make([]interface{}, len(src.schema))
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (e *Merck) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (e *Merck) DefaultEndAt() uint64 {
return 100
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (e *Merck) Info() string {
return "Generates data representative of Merck's pharmaceutical operations."
}
var alg = []string{"NSCLC T", "TC1_28", "NSCLC-LY-21-LA", "NSCLC-LY-22-LA", "NSCLC-LY-23-LA", "NSCLC-LY-24-LA", "NSCLC-LY-25-LA", "NSCLC-LY-37-LA", "NSCLC-LY-38-LA", "NSCLC-LY-39-LA", "NSCLC-LY-40-LA", "NSCLC-LY-41-LA", "NSCLC-LY-42-LA", "NSCLC-LY-43-LA", "NSCLC-LY-44-LA", "NSCLC-LY-45-LA", "NSCLC-LY-46-LA", "NSCLC-LY-47-LA", "NSCLC-LY-48-LA", "NSCLC-LY-59-LA", "NSCLC-LY-60-LA"}
var mask = []string{"not set", "OR Negative", "OR Positive"}
var species = []string{"Homo sapiens", "Rattus", "Sus scrofa"}
var sex = []string{"M", "F", "U"}
var etiology = []string{"Smoking", "Sun light", "Diet Coke", "Sneezing", "Open Cut"}
var morphology = []string{"Lungs", "Skin", "Blood", "Liver", "Stomach"}
var locationTissue = []string{"US", "UK", "CAN", "IND"}
var disease = []string{"Cancer", "Diabetes", "Thyroid", "Leukemia"}
var tissuetype = []string{"MUSC", "NERV", "EPIT", "CONN"}
var procedures_desc = []string{"Cut muscle", "Stich muscle", "Remove skin", "Add oil", "Triple bypass ", "Tape bone", "Remove organ", "Unplug", "Sedate", "Apply bandaid"}
var comments = []string{"aliquip ex ea commodo consequat", "cupidatat non proident, sunt in culpa", "Lorem ipsum dolor sit amet, consectetur", "Excepteur sint occaecat cupidatat non proident", "sunt in culpa qui officia", "deserunt mollit anim id est laborum", "Duis aute irure dolor"}
var analysisregion = []string{"Layer 1", "Layer 2", "Layer 3", "Layer 4", "Layer 5", "Layer 6", "Layer 7", "Layer 8", "Layer 9", "Layer 10"}
var classifierType = []string{"A", "B", "C", "D"}
var classList = []string{"1", "2", "3", "4"}
var outputType = []string{"MASK", "MASK-RCNN", "MASK-NMS"}
var dye = []string{"DAPI", "Autofluorescence", "Opal 690", "Opal 650", "Opal 620", "Opal 570", "Opal 540", "Opal 520"}
var outputImage = []string{"Dye 1", "Dye 2", "Dye 3", "Dye 4", "Dye 5", "Dye 6", "Dye 7", "Dye 8"}
// Ensure MerckSource implements interface.
var _ idk.Source = (*MerckSource)(nil)
// MerckSource is an instance of a source generated
// by the Sourcer implementation Merck.
type MerckSource struct {
cur uint64
endAt uint64
Z *gen.Gen
rand *rand.Rand
algZipf *rand.Zipf
maskZipf *rand.Zipf
analysisregionZipf *rand.Zipf
classListZipf *rand.Zipf
classifierTypeZipf *rand.Zipf
outputTypeZipf *rand.Zipf
dyeZipf *rand.Zipf
outputImageZipf *rand.Zipf
speciesZipf *rand.Zipf
sexZipf *rand.Zipf
commentsZipf *rand.Zipf
tissuetypeZipf *rand.Zipf
locationTissueZipf *rand.Zipf
morphologyZipf *rand.Zipf
etiologyZipf *rand.Zipf
procedures_descZipf *rand.Zipf
diseaseZipf *rand.Zipf
schema []idk.Field
record record
}
// Record implements idk.Source.
func (e *MerckSource) Record() (idk.Record, error) {
if e.cur >= e.endAt {
return nil, io.EOF
}
imageName := e.String(5) + ".tif"
imageLocation := "\\myserver\\" + imageName
// ID allocation
total_records := e.endAt - 1
subjectID := int(math.Ceil(float64(total_records) / float64(128)))
caseID := int(math.Ceil(float64(total_records) / float64(32)))
specimenID := int(math.Ceil(float64(total_records) / float64(8)))
//<-- CELLS BEGIN -->
dye_1_p := e.generate10(1000000, 0)
dye_2_p := e.generate10(1000000, 0)
dye_3_p := e.generate10(1000000, 0)
dye_4_p := e.generate10(1000000, 0)
dye_5_p := e.generate10(1000000, 0)
dye_6_p := e.generate10(1000000, 0)
dye_7_p := e.generate10(1000000, 0)
dye_8_p := e.generate10(1000000, 0)
dye_1_p_n := e.generate10(1000000, 0)
dye_2_p_n := e.generate10(1000000, 0)
dye_3_p_n := e.generate10(1000000, 0)
dye_4_p_n := e.generate10(1000000, 0)
dye_5_p_n := e.generate10(1000000, 0)
dye_6_p_n := e.generate10(1000000, 0)
dye_7_p_n := e.generate10(1000000, 0)
dye_8_p_n := e.generate10(1000000, 0)
dye_1_p_c := e.generate50(1000000, 0)
dye_2_p_c := e.generate50(1000000, 0)
dye_3_p_c := e.generate50(1000000, 0)
dye_4_p_c := e.generate50(1000000, 0)
dye_5_p_c := e.generate50(1000000, 0)
dye_6_p_c := e.generate50(1000000, 0)
dye_7_p_c := e.generate50(1000000, 0)
dye_8_p_c := e.generate50(1000000, 0)
dye_1_p_m := e.generate50(1000000, 0)
dye_2_p_m := e.generate50(1000000, 0)
dye_3_p_m := e.generate50(1000000, 0)
dye_4_p_m := e.generate50(1000000, 0)
dye_5_p_m := e.generate50(1000000, 0)
dye_6_p_m := e.generate50(1000000, 0)
dye_7_p_m := e.generate50(1000000, 0)
dye_8_p_m := e.generate50(1000000, 0)
total_cells := e.generateRandomInt(1000000, 1000)
//<-- CELLS END -->
e.record[0] = e.cur
e.record[1] = imageLocation
e.record[2] = imageName
e.record[3] = alg[e.algZipf.Uint64()]
e.record[4] = startTime.Add(time.Duration(e.Z.R.Int63n(int64(timeSpan))))
e.record[5] = analysisregion[e.analysisregionZipf.Uint64()]
e.record[6] = total_cells
e.record[7] = dye_1_p
e.record[8] = dye_2_p
e.record[9] = dye_3_p
e.record[10] = dye_4_p
e.record[11] = dye_5_p
e.record[12] = dye_6_p
e.record[13] = dye_7_p
e.record[14] = dye_8_p
e.record[15] = dye_1_p_n
e.record[16] = dye_2_p_n
e.record[17] = dye_3_p_n
e.record[18] = dye_4_p_n
e.record[19] = dye_5_p_n
e.record[20] = dye_6_p_n
e.record[21] = dye_7_p_n
e.record[22] = dye_8_p_n
e.record[23] = e.generate50(1000000, 0)
e.record[24] = dye_1_p_c
e.record[25] = dye_2_p_c
e.record[26] = dye_3_p_c
e.record[27] = dye_4_p_c
e.record[28] = dye_5_p_c
e.record[29] = dye_6_p_c
e.record[30] = dye_7_p_c
e.record[31] = dye_8_p_c
e.record[32] = e.generate50(1000000, 0)
e.record[33] = dye_1_p_m
e.record[34] = dye_2_p_m
e.record[35] = dye_3_p_m
e.record[36] = dye_4_p_m
e.record[37] = dye_5_p_m
e.record[38] = dye_6_p_m
e.record[39] = dye_7_p_m
e.record[40] = dye_8_p_m
e.record[41] = e.generate50(1000000, 0)
e.record[42] = float64(dye_1_p) / float64(total_cells)
e.record[43] = float64(dye_3_p) / float64(total_cells)
e.record[44] = float64(dye_3_p) / float64(total_cells)
e.record[45] = float64(dye_4_p) / float64(total_cells)
e.record[46] = float64(dye_5_p) / float64(total_cells)
e.record[47] = float64(dye_6_p) / float64(total_cells)
e.record[48] = float64(dye_7_p) / float64(total_cells)
e.record[49] = float64(dye_8_p) / float64(total_cells)
e.record[50] = float64(dye_1_p_n) / float64(total_cells)
e.record[51] = float64(dye_2_p_n) / float64(total_cells)
e.record[52] = float64(dye_3_p_n) / float64(total_cells)
e.record[53] = float64(dye_4_p_n) / float64(total_cells)
e.record[54] = float64(dye_5_p_n) / float64(total_cells)
e.record[55] = float64(dye_6_p_n) / float64(total_cells)
e.record[56] = float64(dye_7_p_n) / float64(total_cells)
e.record[57] = float64(dye_8_p_n) / float64(total_cells)
e.record[58] = float64(dye_1_p_c) / float64(total_cells)
e.record[59] = float64(dye_2_p_c) / float64(total_cells)
e.record[60] = float64(dye_3_p_c) / float64(total_cells)
e.record[61] = float64(dye_4_p_c) / float64(total_cells)
e.record[62] = float64(dye_5_p_c) / float64(total_cells)
e.record[63] = float64(dye_6_p_c) / float64(total_cells)
e.record[64] = float64(dye_7_p_c) / float64(total_cells)
e.record[65] = float64(dye_8_p_c) / float64(total_cells)
e.record[66] = float64(dye_1_p_m) / float64(total_cells)
e.record[67] = float64(dye_2_p_m) / float64(total_cells)
e.record[68] = float64(dye_3_p_m) / float64(total_cells)
e.record[69] = float64(dye_4_p_m) / float64(total_cells)
e.record[70] = float64(dye_5_p_m) / float64(total_cells)
e.record[71] = float64(dye_6_p_m) / float64(total_cells)
e.record[72] = float64(dye_7_p_m) / float64(total_cells)
e.record[73] = float64(dye_8_p_m) / float64(total_cells)
e.record[74] = e.float50(50, 0)
e.record[75] = e.float50(50, 0)
e.record[76] = e.float50(50, 0)
e.record[77] = e.float50(50, 0)
e.record[78] = e.float50(50, 0)
e.record[79] = e.float50(50, 0)
e.record[80] = e.float50(50, 0)
e.record[81] = e.float50(50, 0)
e.record[82] = e.float50(50, 0)
e.record[83] = e.float50(50, 0)
e.record[84] = e.float50(50, 0)
e.record[85] = e.float50(50, 0)
e.record[86] = e.float50(50, 0)
e.record[87] = e.float50(50, 0)
e.record[88] = e.float50(50, 0)
e.record[89] = e.float50(50, 0)
e.record[90] = e.float50(50, 0)
e.record[91] = e.float50(50, 0)
e.record[92] = e.float50(50, 0)
e.record[93] = e.float50(50, 0)
e.record[94] = e.float50(50, 0)
e.record[95] = e.float50(50, 0)
e.record[96] = e.float50(50, 0)
e.record[97] = e.float50(50, 0)
e.record[98] = e.floatRand(500, 5)
e.record[99] = e.floatRand(500, 5)
e.record[100] = e.floatRand(500, 5)
e.record[101] = e.generateRandomInt(1000000000, 10000000)
e.record[102] = e.generateRandomInt(5, 1)
e.record[103] = e.Z.R.Intn(2) == 0
e.record[104] = classList[e.classListZipf.Uint64()]
e.record[105] = e.Z.R.Intn(2) == 0
e.record[106] = outputType[e.outputTypeZipf.Uint64()]
e.record[107] = dye[e.dyeZipf.Uint64()]
e.record[108] = e.Z.R.Intn(2) == 0
e.record[109] = e.floatRand(1, 0)
e.record[110] = e.floatRand(1, 0)
e.record[111] = e.floatRand(1, 0)
e.record[112] = e.floatRand(1, 0)
e.record[113] = mask[e.maskZipf.Uint64()]
e.record[114] = mask[e.maskZipf.Uint64()]
e.record[115] = mask[e.maskZipf.Uint64()]
e.record[116] = dye[e.dyeZipf.Uint64()]
e.record[117] = e.Z.R.Intn(2) == 0
e.record[118] = e.floatRand(1, 0)
e.record[119] = e.floatRand(1, 0)
e.record[120] = e.floatRand(1, 0)
e.record[121] = e.floatRand(1, 0)
e.record[122] = mask[e.maskZipf.Uint64()]
e.record[123] = mask[e.maskZipf.Uint64()]
e.record[124] = mask[e.maskZipf.Uint64()]
e.record[125] = dye[e.dyeZipf.Uint64()]
e.record[126] = e.Z.R.Intn(2) == 0
e.record[127] = e.floatRand(1, 0)
e.record[128] = e.floatRand(1, 0)
e.record[129] = e.floatRand(1, 0)
e.record[130] = e.floatRand(1, 0)
e.record[131] = mask[e.maskZipf.Uint64()]
e.record[132] = mask[e.maskZipf.Uint64()]
e.record[133] = mask[e.maskZipf.Uint64()]
e.record[134] = dye[e.dyeZipf.Uint64()]
e.record[135] = e.Z.R.Intn(2) == 0
e.record[136] = e.floatRand(1, 0)
e.record[137] = e.floatRand(1, 0)
e.record[138] = e.floatRand(1, 0)
e.record[139] = e.floatRand(1, 0)
e.record[140] = mask[e.maskZipf.Uint64()]
e.record[141] = mask[e.maskZipf.Uint64()]
e.record[142] = mask[e.maskZipf.Uint64()]
e.record[143] = dye[e.dyeZipf.Uint64()]
e.record[144] = e.Z.R.Intn(2) == 0
e.record[145] = e.floatRand(1, 0)
e.record[146] = e.floatRand(1, 0)
e.record[147] = e.floatRand(1, 0)
e.record[148] = e.floatRand(1, 0)
e.record[149] = mask[e.maskZipf.Uint64()]
e.record[150] = mask[e.maskZipf.Uint64()]
e.record[151] = mask[e.maskZipf.Uint64()]
e.record[152] = dye[e.dyeZipf.Uint64()]
e.record[153] = e.Z.R.Intn(2) == 0
e.record[154] = e.floatRand(1, 0)
e.record[155] = e.floatRand(1, 0)
e.record[156] = e.floatRand(1, 0)
e.record[157] = e.floatRand(1, 0)
e.record[158] = mask[e.maskZipf.Uint64()]
e.record[159] = mask[e.maskZipf.Uint64()]
e.record[160] = mask[e.maskZipf.Uint64()]
e.record[161] = dye[e.dyeZipf.Uint64()]
e.record[162] = e.Z.R.Intn(2) == 0
e.record[163] = e.floatRand(1, 0)
e.record[164] = e.floatRand(1, 0)
e.record[165] = e.floatRand(1, 0)
e.record[166] = e.floatRand(1, 0)
e.record[167] = mask[e.maskZipf.Uint64()]
e.record[168] = mask[e.maskZipf.Uint64()]
e.record[169] = mask[e.maskZipf.Uint64()]
e.record[170] = dye[e.dyeZipf.Uint64()]
e.record[171] = e.Z.R.Intn(2) == 0
e.record[172] = e.floatRand(1, 0)
e.record[173] = e.floatRand(1, 0)
e.record[174] = e.floatRand(1, 0)
e.record[175] = e.floatRand(1, 0)
e.record[176] = mask[e.maskZipf.Uint64()]
e.record[177] = mask[e.maskZipf.Uint64()]
e.record[178] = mask[e.maskZipf.Uint64()]
e.record[179] = e.floatRand(1, 0)
e.record[180] = e.floatRand(1, 0)
e.record[181] = e.floatRand(1, 0)
e.record[182] = e.floatRand(1, 0)
e.record[183] = e.floatRand(50, 0)
e.record[184] = e.floatRand(1000, 100)
e.record[185] = e.floatRand(0.5, 0)
e.record[186] = e.floatRand(100, 1)
e.record[187] = e.floatRand(0.5, 0)
e.record[188] = e.floatRand(10, 0)
e.record[189] = e.floatRand(1000, 15)
e.record[190] = outputImage[e.outputImageZipf.Uint64()]
e.record[191] = e.generateRandomInt(1, 0)
e.record[192] = e.floatRand(50, 0)
e.record[193] = e.floatRand(1000, 100)
e.record[194] = e.floatRand(50, 0)
e.record[195] = e.floatRand(1000, 100)
e.record[196] = e.generateRandomInt(subjectID, 0)
e.record[197] = species[e.speciesZipf.Uint64()]
e.record[198] = sex[e.sexZipf.Uint64()]
e.record[199] = startTime.Add(time.Duration(e.Z.R.Int63n(int64(timeSpan))))
e.record[200] = e.generateRandomInt(caseID, 0)
e.record[201] = e.generateRandomInt(95, 0)
e.record[202] = e.generateRandomInt(200, 1)
set := e.generateRandomInt(10, 0)
procedureIDs := make([]uint64, 0, set)
procedureNames := make([]string, 0, set)
procedureDesc := make([]string, 0, set)
for v := 0; v < set; v++ {
procedureIDs = append(procedureIDs, uint64(e.generateRandomInt(10000, 0)))
procedureNames = append(procedureNames, "P"+strconv.Itoa(v))
procedureDesc = append(procedureDesc, procedures_desc[e.procedures_descZipf.Uint64()])
}
e.record[203] = procedureIDs
e.record[204] = procedureNames
e.record[205] = procedureDesc
e.record[206] = e.generateRandomInt(10000, 1)
e.record[207] = e.generateRandomInt(20000, 1)
e.record[208] = comments[e.commentsZipf.Uint64()]
e.record[209] = e.generateRandomInt(10000, 1)
e.record[210] = disease[e.diseaseZipf.Uint64()]
e.record[211] = etiology[e.etiologyZipf.Uint64()]
e.record[212] = morphology[e.morphologyZipf.Uint64()]
e.record[213] = e.generateRandomInt(10000, 1)
e.record[214] = e.generateRandomInt(specimenID, 0)
e.record[215] = tissuetype[e.tissuetypeZipf.Uint64()]
e.record[216] = e.generateRandomInt(500, 1)
e.record[217] = locationTissue[e.locationTissueZipf.Uint64()]
e.cur++
return e.record, nil
}
// Schema implements idk.Source.
func (e *MerckSource) Schema() []idk.Field {
return e.schema
}
func (e *MerckSource) Seed(seed int64) {
e.rand.Seed(seed)
}
var _ Seedable = (*MerckSource)(nil)
const charset = "abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// returns a string with random characters from a predefined charset of a specified length
func (e *MerckSource) StringWithCharset(length int, charset string) string {
b := make([]byte, length)
for i := range b {
b[i] = charset[e.rand.Intn(len(charset))]
}
return string(b)
}
//returns a string with random English alphabets of the specified length
func (e *MerckSource) String(length int) string {
return e.StringWithCharset(length, charset)
}
//returns a random int between a range with a 50% chance of returning a zero.
func (e *MerckSource) generate50(max int, min int) int {
num := e.rand.Intn(100)
numbertoReturn := e.rand.Intn(max-min) + min
switch {
case num < 50:
return 0
default:
return numbertoReturn
}
}
//returns a random int between a range with a 10% chance of returning a zero.
func (e *MerckSource) generate10(max int, min int) int {
num := e.rand.Intn(100)
numbertoReturn := e.rand.Intn(max-min) + min
switch {
case num < 10:
return 0
default:
return numbertoReturn
}
}
//returns a random float between a range with a 50% chance of returning a zero.
func (e *MerckSource) float50(max float64, min float64) float64 {
numbertoReturn := min + e.rand.Float64()*(max-min)
num := e.rand.Intn(100)
switch {
case num < 50:
return 0
default:
return numbertoReturn
}
}
//returns a random float between a range
func (e *MerckSource) floatRand(max float64, min float64) float64 {
return min + e.rand.Float64()*(max-min)
}
//returns a random int between a range
func (e *MerckSource) generateRandomInt(max int, min int) int {
return e.rand.Intn(max-min) + min
}
func (e *MerckSource) Close() error {
return nil
}

248
idk/datagen/network.go Normal file
View file

@ -0,0 +1,248 @@
package datagen
import (
"io"
"math/rand"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
)
// Ensure Network implements interface.
var _ Sourcer = (*Network)(nil)
// Network implements Sourcer.
type Network struct {
schema []idk.Field
}
// NewNetwork returns a new instance of Network.
func NewNetwork(cfg SourceGeneratorConfig) Sourcer {
return &Network{
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.IDField{NameVal: "qos_tier"}, // 1
idk.IntField{NameVal: "source_equip_id"}, // 2
idk.IntField{NameVal: "dest_equip_id"}, // 3
idk.IntField{NameVal: "data_size"}, // 4
idk.StringField{NameVal: "data_type"}, // 5
idk.StringField{NameVal: "customer"}, // 6
idk.IntField{NameVal: "timestamp"}, // 7
},
}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (n *Network) Source(cfg SourceConfig) idk.Source {
src := &NetworkSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(22)),
schema: n.schema,
}
src.typeZipf = rand.NewZipf(src.rand, 1.03, 4, uint64(len(dataTypeStrings))-1)
src.record = make([]interface{}, len(src.schema))
src.record[7] = int(1420070400 + cfg.startFrom)
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (n *Network) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (n *Network) DefaultEndAt() uint64 {
return 100000000
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (n *Network) Info() string {
return "Generates timeseries network transmission data (from_ip, to_ip, bytes) - references data in sites, manufacturer, and equipment."
}
// Ensure NetworkSource implements interface.
var _ idk.Source = (*NetworkSource)(nil)
type NetworkSource struct {
Log logger.Logger
rand *rand.Rand
typeZipf *rand.Zipf
schema []idk.Field
cur, endAt uint64
record record
}
func (s *NetworkSource) Record() (idk.Record, error) {
// ID
if s.cur >= s.endAt {
return nil, io.EOF
}
s.record[0] = s.cur
s.cur++
// QOS
s.record[1] = s.qos()
// TODO - equipment et. al
dataType := s.dataType()
s.record[5] = dataType
s.record[2] = s.srcEquip(dataType)
s.record[3] = s.dstEquip(dataType)
s.record[4] = s.size(dataTypes[dataType])
s.record[6] = s.customer(dataType)
// timestamp
s.record[7] = s.record[7].(int) + s.rand.Intn(3)
return s.record, nil
}
func (s *NetworkSource) customer(dataType string) string {
if dataType == "Transaction" {
return customers[s.rand.Intn(4)]
}
return customers[s.rand.Intn(len(customers))]
}
func (s *NetworkSource) size(dt dataType) int64 {
return s.rand.Int63n(dt.maxSize-dt.minSize) + dt.minSize
}
type dataType struct {
minSize int64
maxSize int64
}
var dataTypes = map[string]dataType{
"Wireless Internet": {
minSize: 51,
maxSize: 22000000, // 22MB
},
"Wired Internet": {
minSize: 63,
maxSize: 40000000, // 40MB
},
"Wired Video": {
minSize: 73,
maxSize: 150000000, // 150MB
},
"Transaction": {
minSize: 12,
maxSize: 1200, // 7MB
},
"Wireless Video": {
minSize: 75,
maxSize: 50000000, // 50MB
},
"Wireless Voice": {
minSize: 17,
maxSize: 7000000, // 7MB
},
"Wired Voice": {
minSize: 19,
maxSize: 10000000, // 10MB
},
}
var dataTypeStrings []string
func init() {
for name := range dataTypes {
dataTypeStrings = append(dataTypeStrings, name)
}
}
func (s *NetworkSource) dataType() string {
return dataTypeStrings[s.typeZipf.Uint64()]
}
func (s *NetworkSource) srcEquip(typ string) int64 {
switch typ {
case "Transaction":
return s.rand.Int63n(1000000) + 35000000 // POS device
case "Wired Video":
return s.rand.Int63n(28000000) + 5000000 // all non BTS devices
case "Wired Internet":
return s.rand.Int63n(28000000) + 5000000 // all non BTS devices
case "Wired Voice":
return s.rand.Int63n(28000000) + 5000000 // all non BTS devices
case "Wireless Video":
return s.rand.Int63n(17000000) + 18000000 // all non CPE devices
case "Wireless Internet":
return s.rand.Int63n(17000000) + 18000000 // all non CPE devices
case "Wireless Voice":
return s.rand.Int63n(17000000) + 18000000 // all non CPE devices
default:
panic("srcEquip: unknown type " + typ)
}
}
func (s *NetworkSource) dstEquip(typ string) int64 {
switch typ {
case "Transaction":
return s.rand.Int63n(15000000) + 18000000 // server and router
case "Wired Video":
return s.rand.Int63n(28000000) + 5000000 // all non BTS devices
case "Wired Internet":
return s.rand.Int63n(28000000) + 5000000 // all non BTS devices
case "Wired Voice":
return s.rand.Int63n(28000000) + 5000000 // all non BTS devices
case "Wireless Video":
return s.rand.Int63n(17000000) + 18000000 // all non CPE devices
case "Wireless Internet":
return s.rand.Int63n(17000000) + 18000000 // all non CPE devices
case "Wireless Voice":
return s.rand.Int63n(17000000) + 18000000 // all non CPE devices
default:
panic("dstEquip: unknown type " + typ)
}
}
func (s *NetworkSource) Schema() []idk.Field {
return s.schema
}
func (s *NetworkSource) Seed(seed int64) {
s.rand.Seed(seed)
}
var _ Seedable = (*NetworkSource)(nil)
// qos returns a QoS value from 1-5 with a 5%/25%/40%/25%/5% weighting.
func (s *NetworkSource) qos() uint64 {
num := s.rand.Intn(100)
switch {
case num < 5:
return 1
case num < 30:
return 2
case num < 70:
return 3
case num < 95:
return 4
default:
return 5
}
}
func (s *NetworkSource) Close() error {
return nil
}

348
idk/datagen/palo_alto.go Normal file
View file

@ -0,0 +1,348 @@
package datagen
import (
"io"
"strconv"
"time"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/datagen/gen"
"github.com/molecula/featurebase/v3/pql"
)
// Ensure PaloAlto implements interface.
var _ Sourcer = (*PaloAlto)(nil)
// PaloAlto implements Sourcer.
type PaloAlto struct {
schema []idk.Field
}
// NewPaloAlto returns a new instance of PaloAlto.
func NewPaloAlto(cfg SourceGeneratorConfig) Sourcer {
return &PaloAlto{
schema: []idk.Field{
idk.StringField{NameVal: "vlan_source"}, // 0) "unknown"
idk.StringField{NameVal: "severity_level"}, // 1) "Normal"
idk.StringField{NameVal: "display_profile_type"}, // 2) "IoT"
idk.BoolField{NameVal: "keep"}, // 3) true
idk.StringField{NameVal: "internal_risk_level"}, // 4) "Low"
idk.IntField{NameVal: "ml_progress"}, // 5) 100
idk.StringField{NameVal: "tenantid"}, // 6) "gjtu7ldl"
idk.StringField{NameVal: "display_oscombined"}, // 7) "Windows CE"
idk.StringField{NameVal: "profileid"}, // 8) "Zebra Technologies Tracking Device"
idk.StringField{NameVal: "risk_main_contributors"}, // 9) "Application Usage"
idk.IntField{NameVal: "profile_date"}, // 10) "2019-03-14T06:09:00.000Z"
idk.StringField{NameVal: "risk_level"}, // 11) "low"
idk.StringArrayField{NameVal: "baseline-learning_category"}, // 12) "payload"
idk.StringField{NameVal: "baseline-state"}, // 13) "stable"
idk.StringField{NameVal: "baseline-protected"}, // 14) "profile model"
idk.IntField{NameVal: "baseline-progress"}, // 15) 100
idk.IntField{NameVal: "baseline-pi"}, // 16) 98
idk.StringArrayField{NameVal: "baseline-invalid_category"}, // 17) "ext_inbound", "ext_outbound"
idk.DecimalField{NameVal: "baseline-anomaly_score-application", Scale: 1}, // 18) 0.1
idk.DecimalField{NameVal: "baseline-anomaly_score-internal", Scale: 4}, // 19) 0.4097
idk.DecimalField{NameVal: "baseline-anomaly_score-protocol", Scale: 4}, // 20) 0.3736
idk.DecimalField{NameVal: "baseline-anomaly_score-external", Scale: 4}, // 21) 0.3548
idk.DecimalField{NameVal: "baseline-anomaly_score-payload", Scale: 4}, // 22) 0.2944
idk.IntField{NameVal: "profile_confidence"}, // 23) 86
idk.StringField{NameVal: "display_osgroup"}, // 24) "Windows CE"
idk.IntField{NameVal: "ml_risk-risk_score"}, // 25) 10
idk.IntField{NameVal: "ml_risk-progress"}, // 26) 100
idk.StringField{NameVal: "ml_risk-severity_level"}, // 27) "Low"
idk.DecimalField{NameVal: "ml_risk-anomaly_score-application", Scale: 2}, // 28) 0.25
idk.DecimalField{NameVal: "ml_risk-anomaly_score-internal", Scale: 2}, // 29) 0.25
idk.DecimalField{NameVal: "ml_risk-anomaly_score-protocol", Scale: 2}, // 30) 0.25
idk.DecimalField{NameVal: "ml_risk-anomaly_score-external", Scale: 2}, // 31) 0.25
idk.DecimalField{NameVal: "ml_risk-anomaly_score-payload", Scale: 2}, // 32) 0.25
idk.IntField{NameVal: "ml_risk-date"}, // 33) "2020-09-22T00:00:00Z"
idk.StringField{NameVal: "risk_level_source"}, // 34) "offline_analytics"
idk.IntField{NameVal: "risk_score"}, // 35) 40
idk.StringField{NameVal: "source"}, // 36) ""
idk.IntField{NameVal: "internal_risk_score"}, // 37) 10
idk.StringField{NameVal: "connect_evtcontent-username"}, // 38) "unknown"
idk.StringField{NameVal: "connect_evtcontent-hostname"}, // 39) "unknown"
idk.BoolField{NameVal: "connect_evtcontent-monitored"}, // 40) true
idk.StringField{NameVal: "connect_evtcontent-ossource"}, // 41) "inspector"
idk.StringField{NameVal: "connect_evtcontent-roles"}, // 42) ""
idk.StringField{NameVal: "connect_evtcontent-osgroup"}, // 43) "Embedded"
idk.StringField{NameVal: "connect_evtcontent-ip"}, // 44) "10.2.171.9"
idk.IntField{NameVal: "connect_evtcontent-vlan"}, // 45) 0
idk.StringField{NameVal: "connect_evtcontent-useragent"}, // 46) "unknown"
idk.StringField{NameVal: "connect_evtcontent-os"}, // 47) "Palm OS"
idk.StringField{NameVal: "connect_evtcontent-osver_src"}, // 48) "offline"
idk.StringField{NameVal: "display_vendor"}, // 49) "Zebra Technologies"
idk.StringField{NameVal: "profile_vendor"}, // 50) "Zebra Technologies"
idk.IntField{NameVal: "ml_risk_score"}, // 51) 10
idk.StringField{NameVal: "subnets"}, // 52) "10.2.170.0/23"
idk.StringField{NameVal: "ml_risk_level"}, // 53) "Low"
idk.StringField{NameVal: "profile_type"}, // 54) "IoT"
idk.IntField{NameVal: "profile_type_score"}, // 55) 100
idk.StringField{NameVal: "display_hostname"}, // 56) "unknown"
idk.StringField{NameVal: "display_os"}, // 57) "Palm OS"
idk.StringField{NameVal: "vendor"}, // 58) "Zebra Technologies"
idk.StringField{NameVal: "siteid"}, // 59) "6"
idk.IntField{NameVal: "progress"}, // 60) 100
idk.IntField{NameVal: "date"}, // 61) "2019-02-09T03:31:08.590Z"
idk.IntField{NameVal: "risk_date"}, // 62) "2018-10-28T00:41:17.534012Z"
idk.StringField{NameVal: "display_vlan"}, // 63) "unknown"
idk.IntField{NameVal: "filter_last_modified"}, // 64) "2017-08-05T01:35:35.429Z"
idk.StringField{NameVal: "profile_category"}, // 65) "Patient Tracking"
idk.StringField{NameVal: "applianceid"}, // 66) "000000000000000000000CC47ADB6556"
idk.StringField{NameVal: "profile_vertical"}, // 67) "Medical"
idk.DecimalField{NameVal: "anomaly_score-application", Scale: 4}, // 68) 0.1442
idk.DecimalField{NameVal: "anomaly_score-internal", Scale: 4}, // 69) 0.1587
idk.DecimalField{NameVal: "anomaly_score-protocol", Scale: 4}, // 70) 0.11750000000000001
idk.DecimalField{NameVal: "anomaly_score-payload", Scale: 1}, // 71) 0.1
idk.DecimalField{NameVal: "anomaly_score-external", Scale: 1}, // 72) 0.1
idk.StringField{NameVal: "profile_osgroup"}, // 73) "Windows CE"
idk.StringField{NameVal: "user_profile_type"}, // 74) "IoT"
idk.StringField{NameVal: "display_dhcp"}, // 75) "Yes"
idk.StringField{NameVal: "profile_type_source"}, // 76) "profiler"
idk.IntField{NameVal: "baseline_update_date"}, // 77) "2019.04.20"
idk.StringField{NameVal: "deviceid"}, // 78) "40:83:de:9e:7d:8c"
idk.StringField{NameVal: "profile_classifier"}, // 79) "OuiClassifier"
idk.StringField{NameVal: "dhcp"}, // 80) "Yes"
idk.StringField{NameVal: "display_profile_category"}, // 81) "Patient Tracking"
idk.StringField{NameVal: "display_profileid"}, // 82) "Macbook Pro"
idk.BoolField{NameVal: "unique"}, // 83) true
idk.BoolField{NameVal: "pii"}, // 84) true
idk.IntField{NameVal: "pii_ts"}, // 85) "2019-01-01T20:15:07.294Z"
idk.StringField{NameVal: "display_osverfirmwarever"}, // 86) ""
idk.StringField{NameVal: "display_wirewireless"}, // 87) "wireless"
idk.StringField{NameVal: "wirewireless"}, // 88) "wireless"
idk.StringField{NameVal: "display_epp_safety"}, // 89) "not_protected"
idk.StringField{NameVal: "mac"}, // 90) "40:83:de:9e:7d:8c"
idk.StringField{NameVal: "foreignaccess"}, // 91) "No"
idk.DecimalField{NameVal: "ai_vendor_confidence", Scale: 2}, // 92) 98.52
idk.IntField{NameVal: "subnets_date"}, // 93) "2020-09-17T18:42:28.691Z"
idk.StringField{NameVal: "display_profile_confidence_source"}, // 94) "profiler"
idk.IntField{NameVal: "display_profile_confidence_date"}, // 95) "2019-03-14T06:09:00Z"
idk.IntField{NameVal: "display_profile_confidence"}, // 96) 86
idk.StringField{NameVal: "display_profile_confidence_level"}, // 97) "70_Medium"
idk.BoolField{NameVal: "filtered"}, // 98)
idk.BoolField{NameVal: "routed"}, // 99)
idk.StringField{NameVal: "object_id"}, // 100)
idk.StringField{NameVal: "country_access_country_code"}, // 101)
idk.BoolField{NameVal: "country_access_malicious"}, // 102)
idk.IntField{NameVal: "country_access_ts"}, // 103)
},
}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (k *PaloAlto) Source(cfg SourceConfig) idk.Source {
src := &PaloAltoSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
schema: k.schema,
}
src.g = gen.New(gen.OptGenSeed(cfg.seed))
src.record = make([]interface{}, len(src.schema))
src.record[17] = []string{}
src.record[5] = make([]byte, 12)
src.record[78] = []byte("40:83:de:9e:7d:8c")
src.record[90] = []byte("40:83:de:9e:7d:8c")
// add object IDs
// there'll be 10,000 unique Object IDs
numUniqueObjectIDs := 10000
objectIDLen := 10
charset := "abcdefghijklmnopqrstuvwxyz0123456789"
objectIDs := make([]string, numUniqueObjectIDs)
for i := 0; i < numUniqueObjectIDs; i++ {
randID := make([]byte, objectIDLen)
for j := range randID {
randID[j] = charset[src.g.R.Intn(len(charset))]
}
objectIDs[i] = string(randID)
}
src.objectIDs = objectIDs
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (k *PaloAlto) PrimaryKeyFields() []string {
return nil
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (k *PaloAlto) DefaultEndAt() uint64 {
return 20000
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (k *PaloAlto) Info() string {
return "Generates data representative of Palo Alto Networks' IoT/medical device security operations. 105 fields of various types, unkeyed."
}
// Ensure PaloAltoSource implements interface.
var _ idk.Source = (*PaloAltoSource)(nil)
// PaloAltoSource is a data generator which generates
// data for all Pilosa field types.
type PaloAltoSource struct {
g *gen.Gen
cur, endAt uint64
schema []idk.Field
record record
objectIDs []string
}
func (k *PaloAltoSource) Record() (idk.Record, error) {
if k.cur >= k.endAt {
return nil, io.EOF
}
company := k.g.StringFromListWeighted(gen.Animals) + " " + k.g.StringFromListWeighted([]string{"Technologies", "Systems", "Associates"})
os := k.g.StringFromListWeighted([]string{"Palm OS", "OSI", "FreeBSD", "BeOS"})
baseTime := time.Date(2014, time.January, 1, 0, 0, 0, 0, time.UTC)
curTime := time.Date(2020, time.November, 1, 0, 0, 0, 0, time.UTC)
durationSeconds := curTime.Sub(baseTime) / time.Second
// Increment the ID.
k.cur++
k.record[0] = k.g.StringFromListWeighted([]string{"unknown", "switch", "router", "gateway"})
k.record[1] = k.g.StringFromListWeighted([]string{"Normal", "Low", "Zero", "Elevated", "High", "Critical"})
k.record[2] = k.g.StringFromListWeighted([]string{"IoT", "Fixed", "Server"})
k.record[3] = k.g.R.Intn(2) == 0
k.record[4] = k.g.StringFromListWeighted([]string{"Low", "Normal", "Zero", "Elevated", "High", "Critical"})
k.record[5] = k.g.R.Int63n(101)
k.record[6] = k.g.StringFromListWeighted([]string{"gjtu7ldl", "C6RKKn", "9pFooX", "SvNzWU", "jCdISA", "y4fsPI", "ID5qFL", "zMpvSO", "DR4pCh",
"5BbmPh", "gjTU0F", "DclwrO", "dpsZkt", "5vGgIy", "FQzIDE", "TW8d5P",
"sxKStY", "1YULKk", "Gpn6r9", "HXy0Ju", "2EVpvj", "3wOt4J", "iXkNkv"})
k.record[7] = k.g.StringFromListWeighted([]string{"Windows CE", "Windows 2000", "Windows XP", "Windows 95", "Mac OS 8", "Solaris", "MS Bob", "Plan 9", "Windows ME Professional Plus Ultimate Turbo Edition"})
k.record[8] = company + " " + k.g.StringFromListWeighted([]string{"Tracking Device", "Pointing Device", "Input Device", "Display", ""})
k.record[9] = k.g.StringFromListWeighted([]string{"Application Usage", "Automated Processes", "Background Scans"})
k.record[10] = baseTime.Unix() + k.g.R.Int63n(int64(durationSeconds))
k.record[11] = k.g.StringFromListWeighted([]string{"low", "normal", "zero", "elevated", "high", "critical"})
k.record[12] = "payload"
k.record[13] = k.g.StringFromListWeighted([]string{"stable", "unstable"})
k.record[14] = "profile model"
k.record[15] = k.g.R.Int63n(101)
k.record[16] = k.g.R.Int63n(101)
k.record[17] = k.g.StringSliceFromList(k.record[17].([]string), []string{"ext_inbound", "ext_outbound", "int_over", "int_under", "ext_across", "int_through"})
k.record[18] = pql.NewDecimal(k.g.R.Int63n(10), 1)
k.record[19] = pql.NewDecimal(k.g.R.Int63n(10000), 4)
k.record[20] = pql.NewDecimal(k.g.R.Int63n(10000), 4)
k.record[21] = pql.NewDecimal(k.g.R.Int63n(10000), 4)
k.record[22] = pql.NewDecimal(k.g.R.Int63n(10000), 4)
k.record[23] = k.g.R.Int63n(100)
k.record[24] = k.g.StringFromListWeighted([]string{"Windows CE", "Windows 2000", "Windows XP", "Windows 95", "Mac OS 8", "Solaris", "MS Bob", "Plan 9", "Windows ME Professional Plus Ultimate Turbo Edition"})
k.record[25] = k.g.R.Int63n(100)
k.record[26] = k.g.R.Int63n(101)
k.record[27] = k.g.StringFromListWeighted([]string{"Low", "Normal", "Zero", "Elevated", "High", "Critical"})
k.record[28] = pql.NewDecimal(k.g.R.Int63n(100), 2) // 0.25
k.record[29] = pql.NewDecimal(k.g.R.Int63n(100), 2) // 0.25
k.record[30] = pql.NewDecimal(k.g.R.Int63n(100), 2) // 0.25
k.record[31] = pql.NewDecimal(k.g.R.Int63n(100), 2) // 0.25
k.record[32] = pql.NewDecimal(k.g.R.Int63n(100), 2) // 0.25
k.record[33] = baseTime.Unix() + k.g.R.Int63n(int64(durationSeconds))
k.record[34] = "offline_analytics"
k.record[35] = k.g.R.Int63n(100)
k.record[36] = k.g.StringFromListWeighted([]string{"snmp", "arp", "wlc", "cmms", "splunk", "bgp", "dhcp", "dns", "ftp", "http", "https",
"imap", "ldap", "mgcp", "mqtt", "nntp", "ntp", "pop", "ptp", "onc/rpc", "rtp",
"rtsp", "rip", "sip", "smtp", "ssh", "telnet", "tls/ssl"})
k.record[37] = k.g.R.Int63n(100)
k.record[38] = "unknown"
k.record[39] = k.g.StringFromListWeighted([]string{"macbook.com", "imac.org", "google.com", "youtube.com", "fb.com", "csdn.net", "apple.com", "stackoverflow.com", "aws.com"})
k.record[40] = k.g.R.Intn(2) == 0
k.record[41] = k.g.StringFromListWeighted([]string{"offline", "inspector", "online", "SNMP", "nmap"})
k.record[42] = ""
k.record[43] = k.g.StringFromListWeighted([]string{"Embedded", "Desktop", "Server", "Workstation", "Handheld", "Hobbyist"})
k.record[44] = "10.2.171.9"
k.record[45] = k.g.R.Int63n(10)
k.record[46] = k.g.StringFromListWeighted([]string{"unknown", "Firefox", "Chrome", "Safari", "Edge", "Internet Explorer", "Netscape Navigator", "Opera", "Iceweasel", "lynx", "IE6"})
k.record[47] = os
k.record[48] = k.g.StringFromListWeighted([]string{"offline", "inspector", "online", "SNMP", "nmap"})
k.record[49] = company
k.record[50] = company
k.record[51] = k.g.R.Int63n(101)
k.record[52] = "10.2.170.0/23"
k.record[53] = k.g.StringFromListWeighted([]string{"Low", "Normal", "Zero", "Elevated", "High", "Critical"})
k.record[54] = k.g.StringFromListWeighted([]string{"IoT", "Fixed", "Server"})
k.record[55] = k.g.R.Int63n(101)
k.record[56] = "unknown"
k.record[57] = os
k.record[58] = company
k.record[59] = strconv.Itoa(k.g.R.Intn(10)) //"6"
k.record[60] = k.g.R.Int63n(101)
k.record[61] = baseTime.Unix() + k.g.R.Int63n(int64(durationSeconds))
k.record[62] = baseTime.Unix() + k.g.R.Int63n(int64(durationSeconds))
k.record[63] = "unknown"
k.record[64] = baseTime.Unix() + k.g.R.Int63n(int64(durationSeconds))
k.record[65] = k.g.StringFromListWeighted([]string{"Patient Tracking", "Doctor Tracking", "Equipment", "Droids You're Looking For"})
k.record[66] = "000000000000000000000CC47AD" + strconv.Itoa(10000+k.g.R.Intn(89000))
k.record[67] = k.g.StringFromListWeighted([]string{"medical", "technology", "retail", "manufacturing", "transportation", "mining", "hospitality"})
k.record[68] = pql.NewDecimal(k.g.R.Int63n(10000), 4)
k.record[69] = pql.NewDecimal(k.g.R.Int63n(10000), 4)
k.record[70] = pql.NewDecimal(k.g.R.Int63n(10000), 4)
k.record[71] = pql.NewDecimal(k.g.R.Int63n(10), 1)
k.record[72] = pql.NewDecimal(k.g.R.Int63n(10), 1)
k.record[73] = k.g.StringFromListWeighted([]string{"Windows CE", "Windows 2000", "Windows XP", "Windows 95", "Mac OS 8", "Solaris", "MS Bob", "Plan 9", "Windows ME Professional Plus Ultimate Turbo Edition"})
k.record[74] = k.g.StringFromListWeighted([]string{"IoT", "Fixed", "Server"})
k.record[75] = k.g.StringFromListWeighted([]string{"Yes", "No", "Unknown"})
k.record[76] = "profiler"
k.record[77] = baseTime.Unix() + k.g.R.Int63n(int64(durationSeconds))
// semi-randomize mac address, cardinality 10k
k.record[78].([]byte)[0] = 48 + byte(k.g.R.Intn(10))
k.record[78].([]byte)[3] = 48 + byte(k.g.R.Intn(10))
k.record[78].([]byte)[12] = 48 + byte(k.g.R.Intn(10))
k.record[78].([]byte)[15] = 48 + byte(k.g.R.Intn(10))
k.record[79] = "OuiClassifier"
k.record[80] = k.g.StringFromListWeighted([]string{"Yes", "No", "Unknown"})
k.record[81] = k.g.StringFromListWeighted([]string{"Patient Tracking", "Doctor Tracking", "Equipment", "Droids You're Looking For"})
k.record[82] = k.g.StringFromListWeighted([]string{"iPhone", "iPad", "Mac-Macbook", "Mac-MacbookPro", "Mac-iMac",
"Google Pixel 5", "Samsung Galaxy Note 20", "OnePlus 8 Pro",
"Mac-MacMini", "Dell G3", "Microsoft Surface Pro", "Thinkpad",
"HP Pavilion x360", "Dell Inspiron 7577", "HP Notebook", "HP Envy", "Asus Zenbook"})
k.record[83] = k.g.R.Intn(2) == 0
k.record[84] = k.g.R.Intn(2) == 0
k.record[85] = baseTime.Unix() + k.g.R.Int63n(int64(durationSeconds))
k.record[86] = ""
k.record[87] = k.g.StringFromListWeighted([]string{"wireless", "wired", "bluetooth", "Qi", "unplugged"})
k.record[88] = k.record[87]
k.record[89] = k.g.StringFromListWeighted([]string{"not_protected", "protected", "double_coverage", "sacked"})
k.record[90] = k.record[78]
k.record[91] = k.g.StringFromListWeighted([]string{"Yes", "No", "Unknown"})
k.record[92] = pql.NewDecimal(k.g.R.Int63n(10000), 2) // 98.52
k.record[93] = baseTime.Unix() + k.g.R.Int63n(int64(durationSeconds))
k.record[94] = "profiler"
k.record[95] = baseTime.Unix() + k.g.R.Int63n(int64(durationSeconds))
k.record[96] = k.g.R.Int63n(100) // 86
k.record[97] = k.g.StringFromListWeighted([]string{"70_Medium", "50_Medium", "30_Medium", "10_Low", "90_High"})
k.record[98] = k.g.R.Intn(4) != 0 // weight towards true
k.record[99] = k.g.R.Intn(4) != 0 // weight towards true
k.record[100] = k.g.StringFromListWeighted(k.objectIDs)
k.record[101] = k.g.StringFromListWeighted([]string{"US", "AT", "AU", "BR", "CA", "CH", "CN", "DE",
"DK", "EC", "EG", "FR", "GB", "IE", "IN", "IT", "JP", "MX", "NL",
"PL", "SA", "SG", "TR", "UG", "ZM"})
k.record[102] = k.g.R.Intn(4) == 0 // weight towards false
// the first value, 15778080, is the timestamp value for 2000-01-01 at
// the minute resolution. The second value used for the int range, 10999979,
// is the duration in minutes between 2000-01-01 and 2020-11-30
k.record[103] = 15778080 + k.g.R.Intn(10999979)
return k.record, nil
}
func (k *PaloAltoSource) Schema() []idk.Field {
return k.schema
}
func (k *PaloAltoSource) Close() error {
return nil
}

View file

@ -0,0 +1,91 @@
package datagen
var equip_ids = []int64{170, 6705, 8998, 15391, 15612, 15749, 21428, 24546, 35501, 36452, 37747, 39447, 39628, 40909,
40946, 44663, 46444, 50279, 55768, 56147, 57967, 58219, 64503, 65026, 67258, 77028, 80852, 86930,
88973, 92380, 94568, 98395, 103641, 105764, 110400, 111418, 125796, 133270, 134283, 136292, 155972, 156180,
164954, 166068, 169949, 175969, 181630, 188810, 189121, 189617, 193660, 194865, 196097, 199576, 205504, 212633,
218376, 219822, 221515, 223200, 225797, 228749, 229343, 230417, 233263, 234602, 240299, 246835, 256838, 257725,
264436, 275532, 278068, 279703, 279957, 283968, 285540, 288279, 289852, 290472, 291381, 297128, 297533, 305117,
307597, 308817, 308882, 311756, 312139, 315301, 317090, 323370, 327423, 329640, 338642, 344825, 356295, 362713,
365282, 365828, 369949, 376736, 377181, 381091, 384663, 387357, 403746, 404146, 411349, 413003, 417046, 417612,
418187, 418659, 422010, 422178, 422750, 426108, 427228, 434152, 438524, 438951, 450396, 452910, 459978, 460929,
466267, 471529, 472060, 479897, 484332, 485665, 495865, 495963, 496292, 498195, 504239, 513424, 514008, 514727,
514742, 518221, 520923, 534051, 535252, 544738, 547757, 548327, 553520, 554143, 554893, 556179, 558376, 559910,
563382, 564769, 572059, 579761, 584028, 587525, 590231, 590471, 595030, 602503, 604893, 605096, 609045, 610622,
615951, 616828, 625267, 625405, 627804, 628584, 645590, 649410, 654265, 655748, 658598, 661862, 662717, 669859,
674301, 681368, 683068, 683443, 689674, 696713, 698216, 720728, 724265, 724466, 735765, 736057, 739503,
741869, 747205, 748781, 752084, 754638, 756812, 759629, 776519, 778482, 780346, 784260, 785979, 787758, 792069,
792128, 792668, 796483, 798071, 799332, 812631, 820151, 830777, 835937, 843404, 848141, 854779, 856719, 864956,
865928, 868442, 877958, 879508, 880188, 888684, 891619, 897742, 898677, 902482, 903074, 903836, 904116, 906172,
910796, 915183, 915622, 921337, 924944, 927204, 927524, 929183, 931793, 933467, 933744, 939942, 940675, 943807,
952816, 961784, 973155, 983443, 990418, 991467, 996985, 1004994, 1006876, 1012563, 1018871, 1021258, 1024536, 1027764,
1035367, 1042971, 1043520, 1045410, 1045909, 1046982, 1061690, 1063084, 1064184, 1064490, 1065741, 1066062, 1067023, 1067144,
1078285, 1080073, 1080681, 1086342, 1099190, 1100930, 1102386, 1103079, 1106776, 1106794, 1113178, 1116643, 1117586, 1121897,
1125231, 1150824, 1151526, 1156509, 1168490, 1169113, 1174033, 1175969, 1177760, 1181016, 1189303, 1194194, 1199435, 1199913,
1205437, 1206431, 1207118, 1207588, 1213100, 1213774, 1216535, 1216671, 1219600, 1229116, 1230334, 1231457, 1240198, 1244987,
1248359, 1248693, 1249288, 1249679, 1252185, 1252192, 1257594, 1258588, 1258837, 1259426, 1264695, 1270705, 1272356, 1275256,
1276195, 1276355, 1286770, 1288261, 1289084, 1295310, 1297805, 1297867, 1300449, 1309905, 1311154, 1314725, 1316373, 1318509,
1323662, 1323872, 1331279, 1341503, 1343486, 1355830, 1360519, 1368186, 1369393, 1377044, 1377355, 1379828, 1384774, 1385131,
1390649, 1390752, 1392016, 1393066, 1397828, 1398403, 1402644, 1407699, 1408193, 1420595, 1421282, 1426560, 1428828, 1447146,
1448096, 1454426, 1456991, 1459313, 1461845, 1464368, 1464775, 1469022, 1471894, 1477498, 1478516, 1479990, 1482303, 1487983,
1489744, 1490877, 1495792, 1504572, 1511350, 1518125, 1518264, 1541773, 1545286, 1547050, 1548522, 1552861, 1564290, 1566414,
1566431, 1577479, 1578614, 1580210, 1586196, 1592158, 1592803, 1609831, 1611124, 1613701, 1618272, 1623993, 1626796, 1630208,
1630632, 1637402, 1640581, 1646833, 1649651, 1653907, 1661388, 1663303, 1677561, 1679624, 1684783, 1687466, 1689207, 1695593,
1699313, 1703642, 1709621, 1711464, 1714225, 1714747, 1718371, 1719938, 1721880, 1722213, 1722378, 1726578, 1731519, 1732614,
1737693, 1738711, 1740693, 1741217, 1741943, 1743220, 1744512, 1750249, 1753969, 1755937, 1756579, 1757821, 1765271, 1771636,
1793789, 1798396, 1806097, 1809291, 1812137, 1824242, 1831634, 1837719, 1842044, 1842278, 1847888, 1850588, 1852475, 1852888,
1856616, 1858738, 1862498, 1866149, 1867368, 1871306, 1871988, 1873287, 1874539, 1883931, 1885816, 1894204, 1894681, 1895614,
1901123, 1903282, 1905423, 1909084, 1923727, 1930663, 1931956, 1932833, 1934820, 1937587, 1940728, 1941918, 1944165, 1946568,
1947987, 1953801, 1953968, 1955186, 1956283, 1970037, 1970939, 1971283, 1971634, 1972028, 1974020, 1982146, 1984706, 1986285,
1988839, 1994294, 1999537, 2001366, 2006323, 2019744, 2020244, 2021998, 2023253, 2024820, 2038708, 2043133, 2050415, 2054573,
2055318, 2055641, 2060181, 2064735, 2066973, 2073400, 2076179, 2077647, 2082933, 2088378, 2091256, 2092254, 2095927, 2096728,
2097256, 2098417, 2104590, 2107812, 2111894, 2113006, 2113172, 2115923, 2123194, 2124775, 2126841, 2127455, 2135914, 2138248,
2138895, 2148831, 2152933, 2154423, 2157534, 2158772, 2165956, 2185189, 2188928, 2189922, 2191770, 2192335, 2197772, 2204293,
2206929, 2207872, 2208524, 2212434, 2213670, 2216288, 2219663, 2222810, 2234302, 2236121, 2244474, 2245406, 2245618, 2247416,
2256856, 2259832, 2266976, 2287105, 2292613, 2300919, 2308247, 2311442, 2317940, 2323171, 2328913, 2332308, 2338815, 2341978,
2342680, 2343207, 2343258, 2347349, 2348530, 2356060, 2357599, 2366448, 2372157, 2373974, 2389857, 2397914, 2408614, 2409025,
2413054, 2415388, 2418060, 2418287, 2431357, 2436957, 2437995, 2440135, 2448182, 2449312, 2450671, 2456767, 2458398, 2465373,
2467950, 2471083, 2478256, 2479620, 2484480, 2485642, 2499422, 2501830, 2503617, 2507695, 2507924, 2514244, 2515993, 2517670,
2521572, 2521758, 2521860, 2522431, 2523551, 2536359, 2542693, 2543890, 2544783, 2551368, 2553326, 2554384, 2555943, 2559870,
2559978, 2560138, 2561731, 2562964, 2563032, 2566555, 2569629, 2572846, 2573293, 2577194, 2587236, 2591677, 2597597, 2598767,
2603379, 2603936, 2610863, 2611226, 2612116, 2617492, 2623462, 2626900, 2627684, 2628938, 2629376, 2630167, 2630971, 2634686,
2635285, 2638959, 2643208, 2650402, 2674968, 2675700, 2682692, 2686250, 2688569, 2698489, 2698756, 2701002, 2708988, 2711781,
2721639, 2722444, 2728982, 2734144, 2736990, 2743465, 2746010, 2746122, 2750772, 2752595, 2760961, 2765330, 2766876, 2768977,
2776873, 2780155, 2783415, 2785459, 2787728, 2789811, 2801913, 2810016, 2821657, 2825572, 2828022, 2835510, 2839090, 2841082,
2845456, 2848734, 2853772, 2860625, 2862994, 2877543, 2877600, 2880718, 2882184, 2883714, 2887228, 2887901, 2889678, 2897354,
2899312, 2904629, 2907542, 2916110, 2925284, 2927109, 2928100, 2929921, 2935813, 2947739, 2948218, 2949067, 2951839, 2952812,
2958330, 2962633, 2969330, 2973762, 2974039, 2977915, 2981914, 2981974, 2993186, 3001478, 3009979, 3010958, 3017945, 3018092,
3021502, 3023793, 3030820, 3048756, 3064636, 3066186, 3069757, 3074327, 3077454, 3082404, 3082597, 3082846, 3090844, 3092798,
3097517, 3117732, 3120551, 3121731, 3123206, 3125144, 3129880, 3134051, 3138887, 3139269, 3146691, 3162623, 3167976, 3171680,
3172092, 3174668, 3176520, 3178406, 3180494, 3181366, 3187286, 3192368, 3195066, 3207053, 3207135, 3213132, 3216378, 3222117,
3235457, 3237770, 3238110, 3239629, 3254968, 3260403, 3271650, 3274503, 3278170, 3279265, 3281370, 3285205, 3287787, 3288274,
3294659, 3297964, 3299741, 3304286, 3304555, 3306973, 3307749, 3318074, 3318193, 3321012, 3332069, 3333286, 3336583, 3357759,
3366036, 3369940, 3373922, 3374020, 3380601, 3380834, 3385680, 3386484, 3388907, 3392291, 3392815, 3402052, 3410562, 3414439,
3420520, 3423275, 3424701, 3425584, 3432973, 3433481, 3435264, 3439456, 3442491, 3447445, 3456075, 3458649, 3466648, 3470869,
3475382, 3476606, 3476621, 3479464, 3487342, 3491444, 3493965, 3499913, 3504016, 3505277, 3510610, 3515067, 3521723, 3525368,
3526858, 3533427, 3539037, 3543634, 3549370, 3554771, 3556188, 3556719, 3557106, 3558716, 3566081, 3568789, 3570470, 3577210,
3577279, 3579450, 3580297, 3581380, 3584824, 3595719, 3596320, 3597828, 3599609, 3600356, 3610056, 3615088, 3615771, 3627152,
3632238, 3633626, 3634000, 3634056, 3638155, 3641404, 3648305, 3650521, 3657979, 3663691, 3668525, 3671821, 3675056, 3690557,
3691160, 3692120, 3699218, 3699407, 3700585, 3702982, 3707485, 3708256, 3708693, 3713871, 3714877, 3719320, 3722712, 3730939,
3732068, 3736951, 3740221, 3742276, 3743132, 3744197, 3747296, 3752418, 3755690, 3760012, 3762328, 3762562, 3764389, 3766506,
3770138, 3774772, 3777522, 3778852, 3782958, 3786682, 3790994, 3798517, 3806492, 3807916, 3812458, 3814356, 3817554, 3819970,
3844018, 3847499, 3853682, 3858273, 3859005, 3859243, 3878788, 3886767, 3888247, 3894022, 3901758, 3905768, 3909665, 3913315,
3916506, 3916508, 3921890, 3932260, 3933067, 3941306, 3945750, 3946479, 3948043, 3948982, 3953119, 3957406, 3963990, 3968883,
3972848, 3973261, 3974825, 3976601, 3977770, 3977958, 3979475, 3984819, 3986007, 3992267, 3993561, 3994389, 4000917, 4001181,
4014442, 4019390, 4022195, 4028877, 4029954, 4032587, 4037102, 4046403, 4048083, 4050063, 4056429, 4059822, 4064935, 4067753,
4068991, 4074173, 4076051, 4078426, 4080941, 4086632, 4089832, 4093462, 4120531, 4126517, 4126989, 4128083, 4134362, 4135763,
4136946, 4142396, 4153635, 4154133, 4155775, 4156353, 4159885, 4163437, 4173644, 4175669, 4175716, 4178444, 4182202, 4188460,
4193633, 4195957, 4196111, 4200175, 4201044, 4203923, 4207626, 4211609, 4212894, 4219233, 4225031, 4227480, 4227943, 4235608,
4237879, 4238186, 4242063, 4246550, 4254375, 4273300, 4273376, 4279125, 4288501, 4292289, 4303360, 4314767, 4319891, 4322988,
4323330, 4331224, 4331940, 4332937, 4338874, 4351993, 4354255, 4367565, 4367809, 4379534, 4391845, 4401627, 4406904, 4409375,
4416007, 4420605, 4422058, 4428228, 4428440, 4429111, 4430316, 4433444, 4442834, 4445844, 4450002, 4459236, 4462603, 4465108,
4470150, 4470783, 4474314, 4477799, 4480164, 4485288, 4499664, 4502245, 4503453, 4515953, 4516662, 4518057, 4518154, 4523451,
4525325, 4528875, 4534950, 4535080, 4536380, 4537342, 4541059, 4542658, 4557608, 4561213, 4564417, 4570249, 4580957, 4581770,
4585987, 4588300, 4601158, 4602739, 4602774, 4604514, 4607159, 4610051, 4616441, 4619168, 4620611, 4623445, 4626226, 4628480,
4637825, 4644310, 4647256, 4660825, 4669516, 4670709, 4677429, 4677486, 4678820, 4679973, 4690245, 4690275, 4695766, 4699852,
4702631, 4704632, 4712369, 4715789, 4719854, 4722861, 4722918, 4725870, 4729612, 4734176, 4741068, 4743653, 4757745, 4760132,
4763429, 4763479, 4771243, 4772347, 4779714, 4779717, 4784801, 4790225, 4790556, 4793134, 4798198, 4799146, 4799455, 4800860,
4811057, 4821071, 4823592, 4834250, 4838501, 4841596, 4842353, 4844741, 4851381, 4855034, 4857285, 4863730, 4864267, 4866872,
4869466, 4869896, 4872492, 4874296, 4877102, 4883864, 4888686, 4891102, 4891984, 4913401, 4913731, 4922248, 4925341, 4927279,
4929436, 4938404, 4955445, 4958196, 4963007, 4963658, 4964288, 4968824, 4970773, 4982366, 4991538, 4991817, 4993291, 4995733,
4996460, 4999789}

View file

@ -0,0 +1,147 @@
package datagen
import (
"io"
"sync"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
)
// Ensure Power1 implements interface.
var _ Sourcer = (*Power1)(nil)
// Power1 implements Sourcer.
type Power1 struct {
startEnds []startEnd
mu sync.Mutex
sourceCall int
}
// NewPower1 returns a new instance of Power1.
func NewPower1(cfg SourceGeneratorConfig) Sourcer {
start := cfg.StartFrom
end := cfg.EndAt
if start == 0 {
start = 100000000
if end == 0 {
end = start + 630
} else {
end += 100000000
}
}
// We need to adjust end to be exclusive.
end++
customCfg := SourceGeneratorConfig{
StartFrom: start,
EndAt: end,
Concurrency: cfg.Concurrency,
}
_, ses, _ := startEnds(customCfg)
return &Power1{
startEnds: ses,
}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (p *Power1) Source(cfg SourceConfig) idk.Source {
p.mu.Lock()
i := p.sourceCall
p.sourceCall++
p.mu.Unlock()
src := &Power1Source{
// We're goind to ignore the passed in start/end,
// and instead used that calculated by NewPower1.
cur: p.startEnds[i].start,
endAt: p.startEnds[i].end,
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.StringField{NameVal: "data_type"}, // 1
idk.IntField{NameVal: "equip_id"}, // 2
idk.StringField{NameVal: "group"}, // 3
idk.IntField{NameVal: "int_value"}, // 4
idk.StringField{NameVal: "string_value"}, // 5
idk.IntField{NameVal: "timestamp"}, // 6
idk.StringField{NameVal: "type"}, // 7
},
}
src.record = make([]interface{}, len(src.schema))
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (p *Power1) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (p *Power1) DefaultEndAt() uint64 {
return 100000630
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (p *Power1) Info() string {
return "Generates data representative of Power Analytics operations (#1)."
}
// Ensure Power1Source implements interface.
var _ idk.Source = (*Power1Source)(nil)
type Power1Source struct {
Log logger.Logger
cur, endAt uint64
schema []idk.Field
record record
}
func (s *Power1Source) Record() (idk.Record, error) {
if s.cur >= s.endAt {
return nil, io.EOF
}
s.record[0] = s.cur
// timestamp
s.record[6] = int(509100000 + (10 * s.cur))
// type
s.record[7] = "Amperage"
// group
s.record[3] = "single"
// equip_id
s.record[2] = 712278 // breaker at a retail site
// data type and string or int value
s.record[1] = "int"
s.record[4] = 30
s.record[5] = nil
s.cur++
return s.record, nil
}
func (s *Power1Source) Schema() []idk.Field {
return s.schema
}
func (s *Power1Source) Close() error {
return nil
}

View file

@ -0,0 +1,125 @@
package datagen
import (
"io"
"math/rand"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
)
// Ensure Power1_2 implements interface.
var _ Sourcer = (*Power1_2)(nil)
// Power1_2 implements Sourcer.
type Power1_2 struct{}
// NewPower1_2 returns a new instance of Power1_2.
func NewPower1_2(cfg SourceGeneratorConfig) Sourcer {
return &Power1_2{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (p *Power1_2) Source(cfg SourceConfig) idk.Source {
src := &Power1_2Source{
cur: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(22)),
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.StringField{NameVal: "data_type"}, // 1
idk.IntField{NameVal: "equip_id"}, // 2
idk.StringField{NameVal: "group"}, // 3
idk.IntField{NameVal: "int_value"}, // 4
idk.StringField{NameVal: "string_value"}, // 5
idk.IntField{NameVal: "timestamp"}, // 6
idk.StringField{NameVal: "type"}, // 7
},
}
src.record = make([]interface{}, len(src.schema))
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (p *Power1_2) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (p *Power1_2) DefaultEndAt() uint64 {
return 3*uint64(len(equip_ids)) - 1
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (p *Power1_2) Info() string {
return "Generates data representative of Power Analytics operations (#2)."
}
// Ensure Power1_2Source implements interface.
var _ idk.Source = (*Power1_2Source)(nil)
type Power1_2Source struct {
Log logger.Logger
rand *rand.Rand
cur, endAt uint64
schema []idk.Field
record record
}
func (s *Power1_2Source) Record() (idk.Record, error) {
if s.cur >= s.endAt {
return nil, io.EOF
}
s.record[0] = 101000000 + s.cur
// timestamp
//s.record[6] = timestamp
// type
s.record[7] = "Amperage"
// group
s.record[3] = "free"
// equip_id
s.record[2] = equip_ids[s.cur%uint64(len(equip_ids))]
// timestamp
s.record[6] = int(1509100001) + 3600*int(s.cur/uint64(len(equip_ids)))
// data type and string or int value
s.record[1] = "int"
s.record[4] = s.rand.Intn(10000) + 50
s.record[5] = nil
s.cur++
return s.record, nil
}
func (s *Power1_2Source) Schema() []idk.Field {
return s.schema
}
func (s *Power1_2Source) Seed(seed int64) {
s.rand.Seed(seed)
}
var _ Seedable = (*Power1_2Source)(nil)
func (s *Power1_2Source) Close() error {
return nil
}

28
idk/datagen/shared.go Normal file
View file

@ -0,0 +1,28 @@
package datagen
import "time"
var customers = []string{"CVS", "McDonald's", "Walmart", "Visa", "Sprint", "Comcast"}
var inStorePayments = []string{"Credit Card", "Debit Card", "Cash", "ACH", "Return"}
var ecomPayments = []string{"Credit Card", "Debit Card", "ACH", "Return"}
var tsTypes = []string{"Voltage", "VoltageRMS", "Amperage", "Resistance", "Frequency", "Message"}
var startDayInt int64
var todayInt int64
var startDate = time.Date(2015, time.January, 1, 0, 0, 0, 0, time.UTC)
var today = time.Date(2019, time.November, 19, 0, 0, 0, 0, time.UTC)
var currentDur = today.Sub(startDate)
var daysSinceStart = int(currentDur / (time.Hour * 24))
func init() {
epoch := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)
start := time.Date(today.Year()-10, today.Month(), today.Day(), today.Hour(), today.Minute(), today.Second(), today.Nanosecond(), today.Location())
startDayInt = int64(start.Sub(epoch) / (time.Hour * 24))
todayInt = int64(today.Sub(epoch) / (time.Hour * 24))
}
func intptr(i int64) *int64 {
return &i
}

37
idk/datagen/sites.data.go Normal file
View file

@ -0,0 +1,37 @@
package datagen
var maintainerDomains = []string{"@datafoundry.com", "@digitalrealty.com", "@globalswitch.us", "@ironmountain.com", "@cyrusone.com", "@chinatelecom-h.com", "@equinix.com", "@chinaunicom.com.hk", "@telehouse.com", "@ntt.com"}
var serviceProviders = []string{"Data Foundry", "Digital Realty Trust", "Global Switch", "Iron Mountain", "CyrusOne", "China Telecom", "Equinix", "China Unicom", "Telehouse", "NTT Communications"}
var lastNames = []string{"smith", "johnson", "williams", "jones", "brown", "davis", "miller", "wilson",
"moore", "taylor", "anderson", "thomas", "jackson", "white", "harris", "martin", "thompson",
"garcia", "martinez", "robinson", "clark", "rodriguez", "lewis", "lee", "walker", "hall",
"allen", "young", "hernandez", "king", "wright", "lopez", "hill", "scott", "green",
"adams", "baker", "gonzalez", "nelson", "carter", "mitchell", "perez", "roberts", "turner",
"phillips", "campbell", "parker", "evans", "edwards", "collins", "stewart", "sanchez", "morris",
"rogers", "reed", "cook", "morgan", "bell", "murphy", "bailey", "rivera", "cooper",
"richardson", "cox", "howard", "ward", "torres", "peterson", "gray", "ramirez", "james",
"watson", "brooks", "kelly", "sanders", "price", "bennett", "wood", "barnes", "ross",
"henderson", "coleman", "jenkins", "perry", "powell", "long", "patterson", "hughes", "flores",
"washington", "butler", "simmons", "foster", "gonzales", "bryant", "alexander", "russell", "griffin",
"diaz", "hayes", "myers", "ford", "hamilton", "graham", "sullivan", "wallace", "woods",
"cole", "west", "jordan", "owens", "reynolds", "fisher", "ellis", "harrison", "gibson",
"mcdonald", "cruz", "marshall", "ortiz", "gomez", "murray", "freeman", "wells", "webb",
"simpson", "stevens", "tucker", "porter", "hunter", "hicks", "crawford", "henry", "boyd",
"mason", "morales", "kennedy", "warren", "dixon", "ramos", "reyes", "burns", "gordon",
"shaw", "holmes", "rice", "robertson", "hunt", "black", "daniels", "palmer", "mills",
"nichols", "grant", "knight", "ferguson", "rose", "stone", "hawkins", "dunn", "perkins",
"hudson", "spencer", "gardner", "stephens", "payne", "pierce", "berry", "matthews", "arnold",
"wagner", "willis", "ray", "watkins", "olson", "carroll", "duncan", "snyder", "hart",
"cunningham", "bradley", "lane", "andrews", "ruiz", "harper", "fox", "riley", "armstrong",
"carpenter", "weaver", "greene", "lawrence", "elliott", "chavez", "sims", "austin", "peters",
"kelley", "franklin", "lawson", "fields", "gutierrez", "ryan", "schmidt", "carr", "vasquez",
"castillo", "wheeler", "chapman", "oliver", "montgomery", "richards", "williamson", "johnston", "banks",
"meyer", "bishop", "mccoy", "howell", "alvarez", "morrison", "hansen", "fernandez", "garza",
"harvey", "little", "burton", "stanley", "nguyen", "george", "jacobs", "reid", "kim",
"fuller", "lynch", "dean", "gilbert", "garrett", "romero", "welch", "larson", "frazier",
"burke", "hanson", "day", "mendoza", "moreno", "bowman", "medina", "fowler", "brewer",
"hoffman", "carlson", "silva", "pearson", "holland", "douglas", "fleming", "jensen", "vargas",
"byrd", "davidson", "hopkins", "may", "terry", "herrera", "wade", "soto", "walters",
"curtis", "neal", "caldwell", "lowe"}

211
idk/datagen/sites.go Normal file
View file

@ -0,0 +1,211 @@
package datagen
import (
"errors"
"io"
"math/rand"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
)
// Ensure Site implements interface.
var _ Sourcer = (*Site)(nil)
// Site implements Sourcer.
type Site struct{}
// NewSite returns a new instance of Site.
func NewSite(cfg SourceGeneratorConfig) Sourcer {
return &Site{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (s *Site) Source(cfg SourceConfig) idk.Source {
src := &SiteSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(22)),
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.DecimalField{NameVal: "latitude", Scale: 4}, // 1
idk.DecimalField{NameVal: "longitude", Scale: 4}, // 2
idk.StringField{NameVal: "maintainer"}, // 3
idk.StringField{NameVal: "classification"}, // 4
idk.StringField{NameVal: "type"}, // 5
idk.StringField{NameVal: "service_provider"}, // 6
idk.StringField{NameVal: "region"}, // 7
idk.DateIntField{NameVal: "last_inspection"}, // 8
},
}
src.providerZipf = rand.NewZipf(src.rand, 1.03, 3, uint64(len(serviceProviders))-1)
src.record = make([]interface{}, len(src.schema))
src.record[7] = int(1420070400)
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (s *Site) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (s *Site) DefaultEndAt() uint64 {
return 4688000 - 1
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (s *Site) Info() string {
return "Generates site data (customer, size, location) - includes a foreign key for network_ts, power_ts, transaction_ts."
}
// Ensure SiteSource implements interface.
var _ idk.Source = (*SiteSource)(nil)
type SiteSource struct {
Log logger.Logger
rand *rand.Rand
providerZipf *rand.Zipf
schema []idk.Field
cur, endAt uint64
record record
}
func (s *SiteSource) Record() (idk.Record, error) {
if s.cur >= s.endAt {
return nil, io.EOF
}
s.record[0] = s.cur
// lat/lon
longitude, latitude := s.location()
s.record[1] = latitude
s.record[2] = longitude
// service provider
var domain string
s.record[6], domain = s.serviceProviderDomain()
// maintainer
s.record[3] = s.emailName() + domain
// classification
s.record[4] = s.classification()
// type
s.record[5] = s.siteType(s.cur)
if s.record[5].(string) == "" {
return nil, errors.New("exhausted sites data set")
}
// region
s.record[7] = region(latitude, longitude)
// last_inspection
s.record[8] = startDayInt + s.rand.Int63n(365*10-200)
s.cur++
return s.record, nil
}
func (s *SiteSource) classification() string {
num := s.rand.Intn(100)
switch {
case num < 3:
return "Tier 1"
case num < 20:
return "Tier 2"
case num < 40:
return "Tier 3"
case num < 75:
return "Tier 4"
default:
return "Tier 5"
}
}
func (s *SiteSource) serviceProviderDomain() (string, string) {
idx := s.providerZipf.Uint64()
return serviceProviders[idx], maintainerDomains[idx]
}
func (s *SiteSource) emailName() string {
idx := s.rand.Intn(len(lastNames))
lastName := lastNames[idx]
initial := uint8(s.rand.Intn(26) + 97)
return string([]byte{initial, '.'}) + lastName
}
func (s *SiteSource) siteType(id uint64) string {
switch {
case id < 1000:
return "MSC" // 1k
case id < 8000:
return "Headend" // 7k
case id < 33000:
return "Central Office" // 25k
case id < 88000:
return "Substation" // 55k
case id < 488000:
return "Retail" // 400k
case id < 888000:
return "Cell Site" // 400k
case id < 1688000:
return "NOC" // 800k
case id < 4688000:
return "Data Center" // 3M
default:
return ""
}
}
func (s *SiteSource) location() (lon float64, lat float64) {
loc := locations[s.rand.Intn(len(locations))]
off1, off2 := (s.rand.Float64()-0.5)/50, (s.rand.Float64()-0.5)/50
return loc.lon + off1, loc.lat + off2
}
func region(latitude, longitude float64) string {
if longitude < 80 {
return "East"
}
if longitude > 105 {
return "West"
}
if latitude < 39 {
return "South"
}
return "North"
}
func (s *SiteSource) Schema() []idk.Field {
return s.schema
}
func (s *SiteSource) Seed(seed int64) {
s.rand.Seed(seed)
}
var _ Seedable = (*SiteSource)(nil)
func (s *SiteSource) Close() error {
return nil
}

124
idk/datagen/sizing.go Normal file
View file

@ -0,0 +1,124 @@
package datagen
import (
"io"
"math/rand"
pilosaclient "github.com/molecula/featurebase/v3/client"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
)
// Ensure Sizing implements interface.
var _ Sourcer = (*Sizing)(nil)
// Sizing implements Sourcer.
type Sizing struct{}
// NewSizing returns a new instance of Sizing.
func NewSizing(cfg SourceGeneratorConfig) Sourcer {
return &Sizing{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (s *Sizing) Source(cfg SourceConfig) idk.Source {
src := &SizingSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(22)),
schema: []idk.Field{
idk.IDField{NameVal: "id"},
idk.IntField{NameVal: "eightbit_random"},
idk.IntField{NameVal: "sixteenbit_random"},
idk.IntField{NameVal: "thirtytwobit_random"},
idk.IntField{NameVal: "sixtythreebit_random"},
idk.IntField{NameVal: "eightbit_zipf"},
idk.IntField{NameVal: "sixteenbit_zipf"},
idk.IntField{NameVal: "thirtytwobit_zipf"},
idk.IntField{NameVal: "sixtythreebit_zipf"},
},
}
src.record = make([]interface{}, len(src.schema))
src.zipf8 = rand.NewZipf(src.rand, 1.01, 4, 255)
src.zipf16 = rand.NewZipf(src.rand, 1.01, 4, 65535)
src.zipf32 = rand.NewZipf(src.rand, 1.01, 4, 4294967295)
src.zipf63 = rand.NewZipf(src.rand, 1.01, 4, 9223372036854775807)
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (s *Sizing) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (s *Sizing) DefaultEndAt() uint64 {
return pilosaclient.DefaultShardWidth - 1 // one shard width
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (s *Sizing) Info() string {
return "Generates data for estimating index size relative to original data size."
}
// Ensure SizingSource implements interface.
var _ idk.Source = (*SizingSource)(nil)
// SizingSource is an idk.Source meant to generate data which is
// helpful in determining the on-disk footprint of different types of
// data which can be extrapolated to help estimate necessary
// infrastructure size for various data. Typically one shard width of
// data is generated.
type SizingSource struct {
Log logger.Logger
cur, endAt uint64
rand *rand.Rand
zipf8 *rand.Zipf
zipf16 *rand.Zipf
zipf32 *rand.Zipf
zipf63 *rand.Zipf
schema []idk.Field
record record
}
func (s *SizingSource) Record() (idk.Record, error) {
if s.cur >= s.endAt {
return nil, io.EOF
}
s.record[0] = s.cur
s.record[1] = s.rand.Intn(255)
s.record[2] = s.rand.Intn(65535)
s.record[3] = s.rand.Intn(4294967295)
s.record[4] = s.rand.Intn(9223372036854775807)
s.record[5] = int64(s.zipf8.Uint64())
s.record[6] = int64(s.zipf16.Uint64())
s.record[7] = int64(s.zipf32.Uint64())
s.record[8] = int64(s.zipf63.Uint64())
s.cur++
return s.record, nil
}
func (s *SizingSource) Schema() []idk.Field {
return s.schema
}
func (s *SizingSource) Seed(seed int64) {
s.rand.Seed(seed)
}
var _ Seedable = (*SizingSource)(nil)
func (s *SizingSource) Close() error {
return nil
}

218
idk/datagen/skills.data.go Normal file
View file

@ -0,0 +1,218 @@
package datagen
var skills = []string{
"Data Entry",
"Answering Phones",
"Billing",
"Bookkeeping (Excel, Turbotax)",
"Scheduling",
"Ms Office",
"Office Equipment",
"Quickbooks",
"Shipping",
"Welcoming Visitors",
"Salesforce",
"Calendar Management",
"Organization",
"Time Management",
"Problem Solving",
"Attention To Detail",
"Adaptability",
"Written Communication",
"Email Platforms (E.G., Gmail And iCloud Mail)",
"Word Processing Software (E.G., Google Docs)",
"Spreadsheet Software",
"Databases",
"Digital Calendars",
"Prioritization",
"Product Knowledge",
"Lead Qualification",
"Lead Prospecting",
"Customer Needs Analysis",
"Referral Marketing",
"Contract Negotiation",
"Self Motivation",
"Increasing Customer Lifetime Value (Clv)",
"Reducing Customer Acquisition Cost (Cac)",
"CRM Software (Salesforce, Hubspot, Zoho, Freshsales)",
"POS Skills",
"Cashier Skills",
"Problem Solving",
"Patience",
"Knowledge Of Products And Services",
"Adaptability",
"Customer Service",
"Communication",
"Math Skills",
"Organization",
"Time Management",
"Negotiation",
"Persuasion",
"Patience",
"Diplomacy",
"Cold Calling",
"Customer Relationship Management (CRM)",
"Lead Generation",
"Buyer-responsive Selling",
"Buyer Engagement",
"Teamwork",
"Patient Assessment",
"Taking Vital Signs",
"Patient Care",
"Recording Patient Medical History",
"Wound Dressing And Care",
"Urgent And Emergency Care",
"Record-keeping",
"Patient Education",
"NIH Stroke Scale Patient Assessment",
"Electronic Medical Record (EMR)",
"Medicine Administration",
"Blood Pressure Monitoring",
"Phlebotomy",
"Rehabilitation Therapy",
"Hygiene Assistance",
"Use Of X-ray, MRI, Cat Scans",
"Meditech",
"Glucose Checks",
"Electronic Heart Record (EHR)",
"Time Management",
"Prioritization",
"Decision-Making Ability",
"Critical Thinking",
"Leadership",
"Thorough Understanding Of HIPPA And Privacy Policies",
"Ability To Take And Record Vital Signs",
"CPR And First-Aid Certifications",
"Perform And Evaluate Diagnostic Tests",
"Maintain Patient Charts",
"Programming Languages",
"Web Development",
"Data Structures",
"Open Source Experience",
"Security",
"Machine Learning",
"Debugging",
"UX/UI",
"Front-End & Back-End Development",
"Cloud Management",
"Agile Development",
"HTML/CSS",
"CSS Preprocessors",
"Javascript",
"Wordpress",
"Graphic User Interfaces (GUI)",
"Git/Version Control (Github, gitlab)",
"Search Engine Optimization (SEO)",
"Application Programming Interface (API)",
"Adobe Photoshop, InDesign",
"Content Management Systems (CMS)",
"Testing/Debugging",
"Responsive Design Principles",
"Mobile and Web Development (e.g. iOS, Android)",
"STEM Skills",
"CAD",
"Design",
"Prototyping",
"Testing",
"Troubleshooting",
"Project Launch",
"Lean Manufacturing",
"Workflow Development",
"Computer Skills",
"SolidWorks",
"Budgeting",
"Technical Report Writing",
"Programming Languages HTML, CSS, CRM tools",
"SEO (SEMRush, WordPress, and Ahrefs)",
"SEM (i.e., Google Adwords)",
"PPC",
"CRO and A/B Testing",
"Social Media Marketing and Paid Social Media Advertising",
"Sales Funnel Management",
"CMS Tools (WordPress, Weebly)",
"Graphic Design Skills (Adobe Creative Suite)",
"Email Marketing (MailChimp, Constant Contact)",
"Email Automation",
"Data Visualization",
"CPC",
"Typography",
"Print Design",
"Photography and Branding",
"Communication",
"Creativity",
"Data Analytics (Google Analytics )",
"Web Analytics",
"Adaptability",
"Organized",
"Email Writing",
"Google Adwords",
"Social Media And Mobile Marketing",
"Paid Social Media Advertisements",
"Consumer Behavior Drivers",
"Brand Management",
"B2b Marketing",
"Writing Advertising Copy",
"Soliciting Feedback From Customers",
"Cutting Costs",
"Leadership skills",
"Task Delegation",
"Strategic Management",
"Negotiation",
"Planning",
"Proposal writing",
"Problem-solving",
"Innovation",
"Charisma",
"Problem solving",
"Personable",
"Website Management",
"Social Media Outreach",
"Video Production",
"Data Visualization",
"Campaign Management",
"Photo Editing",
"Editing",
"Typography",
"Logo Creation",
"Digital Printing",
"Interactive Media Design",
"Color Sense & Theory",
"Ad Design",
"Social Media Publishing",
"UX Design",
"Storytelling",
"Financial Analysis",
"Consumer Research",
"Statistical Analysis",
"Drawing",
"Design",
"Videography",
"Technical Writing",
"Mobile and Web Development (e.g. iOS, Android)",
"Agile Project Management (Kanban)",
"Managing Cross-Functional Teams",
"Scrum Management",
"Performance Tracking",
"Financial Modelling",
"Ideation Leadership",
"Feature Definition",
"Forecasting",
"Profit and Loss",
"Scope Management",
"Project Lifecycle Management",
"Meeting Facilitation",
"Leadership",
"Communication",
"Critical Thinking",
"Time Management",
"Mentoring",
"Risk Management",
"Negotiation",
"Cost Management",
"Data Analysis",
"Collaborative",
"Collaborative Programs (Slack, WhatsApp, Dropbox)",
"Wordpress",
"Adobe Creative Suite",
"Performance Tracking",
}

94
idk/datagen/stringpk.go Normal file
View file

@ -0,0 +1,94 @@
package datagen
import (
"io"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/datagen/gen"
)
// Ensure StringPK implements interface.
var _ Sourcer = (*StringPK)(nil)
// StringPK implements Sourcer.
type StringPK struct{}
// NewStringPK returns a new instance of StringPK.
func NewStringPK(cfg SourceGeneratorConfig) Sourcer {
return &StringPK{}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (s *StringPK) Source(cfg SourceConfig) idk.Source {
sps := &StringPKSource{
record: make(record, 2),
n: cfg.startFrom,
endAt: cfg.endAt,
}
sps.record[0] = make([]byte, 12)
sps.g = gen.New(gen.OptGenSeed(cfg.seed))
return sps
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (s *StringPK) PrimaryKeyFields() []string {
return []string{"pk"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (s *StringPK) DefaultEndAt() uint64 {
return 10000000
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (s *StringPK) Info() string {
return "TODO"
}
// Ensure StringPKSource implements interface.
var _ idk.Source = (*StringPKSource)(nil)
type StringPKSource struct {
g *gen.Gen
endAt uint64
n uint64
record record
}
func (s *StringPKSource) Record() (idk.Record, error) {
if s.n >= s.endAt {
return nil, io.EOF
}
s.g.AlphaUpper(s.record[0].([]byte))
s.record[1] = s.g.R.Intn(10)
s.n++
return s.record, nil
}
func (s *StringPKSource) Schema() []idk.Field {
return []idk.Field{
idk.StringField{NameVal: "pk"},
idk.IDField{NameVal: "field1"},
}
}
func (s *StringPKSource) Seed(seed int64) {
s.g.R.Seed(seed)
}
var _ Seedable = (*StringPKSource)(nil)
func (s *StringPKSource) Close() error {
return nil
}

119
idk/datagen/testdata/custom.yaml vendored Normal file
View file

@ -0,0 +1,119 @@
fields:
- name: "an_id"
type: "uint" # (default IDField (non-mutex))
distribution: "sequential"
min: 0
max: 1000000
repeat: false # if false, data generation stops when we hit >= max. only available with sequential
step: 1
- name: "an_int"
type: "int" # (default IntField)
distribution: "uniform" # uniform or zipfian
min: 0
max: 5
null_chance: 0.01
- name: "a_zipf_int"
type: "int" # (default IntField)
distribution: "zipfian"
min: 0
max: 100
s: 1.1
v: 5.1
- name: "a_string_from_file"
type: "string" # (default StringField (non-mutex))
source_file: "testdata/file_with_line_delimited_values" # this will be used if not empty
distribution: "zipfian" # uniform or zipfian
s: 1.1 # s and v are used for zipfian
v: 5.1
- name: "a_random_string"
type: "string" # (default StringField (non-mutex))
generator_type: "random-string" # used to generate random strings rather than pulling from known set
min_len: 8
max_len: 12
charset: "AB" # set of possible characters to pull from when generating random string
- name: "a_string_set"
type: "string-set" # (default StringArrayField)
generator_type: "random-string" # used to generate random strings rather than pulling from known. "distribution" is ignored.
min_len: 4
max_len: 4
charset: "0123456789ABCDEF"
min_num: 0 # minimum number of strings in each value (default 0)
max_num: 10 # max number of strings (default to cardinality of source)
- name: "a_string_set_from_file"
type: "string-set" # (default StringArrayField)
source_file: "testdata/file_with_line_delimited_values" # this will be used if not empty
generator_type: "random-string" # used to generate random strings rather than pulling from known
min_num: 0 # minimum number of strings in each value (default 0)
max_num: 10 # max number of strings (default to cardinality of source)
- name: "an_id_set"
type: "uint-set" # (default IDArrayField)
min: 0
max: 1000
distribution: "uniform"
min_num: 1
max_num: 3
- name: "a_timestamp_field"
type: "timestamp" # (default TimestampField)
min_date: 2006-01-02T15:04:05.001Z # RFC3339Nano
max_date: 2010-01-02T15:04:05.001Z # RFC3339Nano
distribution: "increasing" # only "increasing" is supported right now
min_step_duration: "1s"
max_step_duration: "1h" # generated values will add randomly between 1s and 1h to previous value starting at min_date.
repeat: false # stop at > max_date unless repeat=true... then go back to min.
- name: "a_decimal_field"
type: "float" # use idk_params to choose a scale
min_float: 0.2
max_float: 1.5
distribution: "uniform" # only supported value
- name: "gender"
type: string
source_file: "testdata/gender.txt"
distribution: "fixed"
- name: "shifting_key"
type: string
# The shifting distribution is meant to simulate a set of strings
# that evolves over time. We always generate a number in a
# particular range defined by the cardinality, but we always add
# an amount to that number which starts at 0 and increases over
# time. In this way, some numbers will fall out and never be
# generated again, and new numbers will come in. We map each
# number to a string by formatting it and padding with zeros to
# get the desired length.
distribution: "shifting"
step: 10 # step for "shifting" distribution means how often do we shift. Every time we generate a value, we'll add an amount to it, every <step> generations, we'll increase the amount by one.
cardinality: 123123123
max_len: 32
s: 1.01
v: 200
null_chance: 0
# idk_params describe how data from "fields" should be ingested by IDK
idk_params:
primary_key_config:
field: "id" # if this is a single field named "id" then we'll use uint IDs, if it's empty we'll autogen ids, and if it's anything else we'll do string keys... yes this is a bit hacky, needs to be cleaned up.
# fields is keyed by names of fields from top level "fields". It is
# not required that all fields appear here, those that don't will
# use the default ingestion.
fields:
an_id:
- type: "ID"
mutex: false
name: "id"
a_decimal_field:
- type: "Decimal"
scale: 4
a_timestamp_field:
- type: "RecordTime"
layout: "2006-01-02T15:04:05Z"
epoch: 1970-01-01T00:00:00.0Z
name: "na"
- type: "Timestamp"
name: "a_timestamp_field"
# a_string:
# - type: "StringField"
# keyed: true
# mutex: true
a_string_set:
- type: "StringArray"
time_quantum: "YMD"
ttl: "0s"

View file

@ -0,0 +1,5 @@
blah
blee
bloo
bleh
blin

5
idk/datagen/testdata/gender.txt vendored Normal file
View file

@ -0,0 +1,5 @@
Male,0.48
Female,0.48
Transgender,0.01
Other,0.01
Unspecified,0.02

37
idk/datagen/testdata/schema.json vendored Normal file
View file

@ -0,0 +1,37 @@
[
{
"name": "uuid",
"path": [
"uuid"
],
"type": "string"
},
{
"name": "type",
"path": [
"type"
],
"type": "id"
},
{
"name": "segid",
"path": [
"slice"
],
"type": "ids"
},
{
"name": "timestamp",
"path": [
"ts"
],
"type": "recordTime"
},
{
"name": "lastupdated",
"path": [
"lastupdated"
],
"type": "timestamp"
}
]

48
idk/datagen/testdata/tremor_keys.yaml vendored Normal file
View file

@ -0,0 +1,48 @@
fields:
- name: "uuid"
type: string
distribution: "shifting"
step: 10 # step for "shifting" distribution means how often do we shift. Every time we generate a value, we'll add an amount to it, every <step> generations, we'll increase the amount by one.
cardinality: 400000000
max_len: 32
s: 1.01
v: 200
null_chance: 0
- name: "type"
type: "int" # (default IntField)
min: 0
max: 3
distribution: "zipfian"
s: 1.1
v: 5.1
- name: "ts"
type: "timestamp"
min_date: 2006-01-02T15:04:05.001Z # RFC3339Nano
max_date: 2022-01-02T15:04:05.001Z # RFC3339Nano
distribution: "increasing" # only "increasing" is supported right now
min_step_duration: "1ms"
max_step_duration: "200ms"
- name: "lastupdated"
type: "timestamp"
min_date: 2006-01-02T15:04:05.001Z # RFC3339Nano
max_date: 2022-01-02T15:04:05.001Z # RFC3339Nano
distribution: "increasing" # only "increasing" is supported right now
min_step_duration: "1ms"
max_step_duration: "200ms"
- name: "slice"
type: "uint-set" # (default IDArrayField)
min: 0
max: 35000
distribution: "zipfian"
s: 1.1
v: 5.1
min_num: 1
max_num: 50
# idk_params describe how data from "fields" should be ingested by IDK
idk_params:
primary_key_config:
field: "uuid" # if this is a single field named "id" then we'll use uint IDs, if it's empty we'll autogen ids, and if it's anything else we'll do string keys... yes this is a bit hacky, needs to be cleaned up.
# fields is keyed by names of fields from top level "fields". It is
# not required that all fields appear here, those that don't will
# use the default ingestion.

258
idk/datagen/texas_health.go Normal file
View file

@ -0,0 +1,258 @@
package datagen
import (
"fmt"
"io"
"math/rand"
"time"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/idk/datagen/gen"
)
// Ensure TexasHealth implements interface.
var _ Sourcer = (*TexasHealth)(nil)
// TexasHealth implements Sourcer
type TexasHealth struct{}
// NewTexasHealth returns a new instance of NewTexasHealth.
func NewTexasHealth(cfg SourceGeneratorConfig) Sourcer {
return &TexasHealth{}
}
type ciscoNetworkEvent struct {
id string
possibleTypes []string
protocol string
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (t *TexasHealth) Source(cfg SourceConfig) idk.Source {
startTime := time.Date(2020, time.December, 1, 0, 0, 0, 0, time.UTC)
src := &TexasHealthSource{
cur: cfg.startFrom,
recordsGenForBucket: 20,
currBucketSize: 0,
currTimestamp: startTime,
endAt: cfg.endAt,
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.IDField{NameVal: "event_id"}, // 1
idk.StringField{NameVal: "type"}, // 2
idk.StringField{NameVal: "protocol"}, // 3
idk.IntField{NameVal: "probe"}, // 4
idk.IntField{NameVal: "severity"}, // 5
idk.StringField{NameVal: "level"}, // 6
idk.StringField{NameVal: "from_interface"}, // 7
idk.StringField{NameVal: "from_address"}, // 8
idk.StringField{NameVal: "from_port"}, // 9
idk.StringField{NameVal: "to_interface"}, // 10
idk.StringField{NameVal: "to_address"}, // 11
idk.StringField{NameVal: "to_port"}, // 12
idk.IntField{NameVal: "duration_time"}, // 13
idk.IntField{NameVal: "bytes"}, // 14
idk.StringField{NameVal: "hostname"}, // 15
idk.TimestampField{NameVal: "timestamp", Unit: "s"}, // 16
idk.StringField{NameVal: "timestamp_str"}, // 17
},
}
src.g = gen.New(gen.OptGenSeed(cfg.seed))
src.possibleEvents = []ciscoNetworkEvent{
{
id: "302013",
possibleTypes: []string{"Built inbound", "Built outbound"},
protocol: "TCP",
},
{
id: "302014",
possibleTypes: []string{"Teardown"},
protocol: "TCP",
},
{
id: "302015",
possibleTypes: []string{"Built inbound", "Built outbound"},
protocol: "UDP",
}, {
id: "302016",
possibleTypes: []string{"Teardown"},
protocol: "UDP",
}, {
id: "302020",
possibleTypes: []string{"Built inbound", "Built outbound"},
protocol: "ICMP",
}, {
id: "302021",
possibleTypes: []string{"Teardown"},
protocol: "ICMP",
},
}
src.possibleLogLevels = []string{"Debug", "Informational", "Notification", "Warning", "Error", "Critical"}
// generate hostnames
var hostnames []string
prefixes := []string{"dbprdfw", "dbrwanasa", "dbdgstsw", "dbrrassa",
"phdasa", "dbrprdfw", "hfwasa", "phamasa", "thdrasa", "thalasa",
}
for _, prefix := range prefixes {
for i := 1; i <= 10; i++ {
hostnames = append(hostnames, fmt.Sprintf("%s%02d", prefix, i))
}
}
src.possibleHostnames = hostnames
// generate ip addresses
var ipAddrs []string
for i := 1; i <= 100; i++ {
ipAddrs = append(ipAddrs, fmt.Sprintf("10.164.124.%d", i))
}
src.possibleIPAddrs = ipAddrs
// ports
src.possiblePorts = []string{"443", "80", "22", "23", "40", "8080",
"53", "88", "115", "123", "143", "20", "21", "25", "101",
}
// interfaces
src.possibleFromInterfaces = []string{"faddr",
"Security_Cameras_250", "LAB", "PROD",
"INSIDE", "provider", "OUTSIDE", "management",
"Inside-DataCenter-DBR", "ATT_SBC_Outside", "Outside-WAN-DBR"}
src.possibleToInterfaces = []string{"laddr",
"OUTSIDE", "Inside-DataCenter", "DBR", "ATT_SBC_Inside", "Outside-WAN-DBR"}
// rand.Zipf for generating bytes and duration
src.randBytes = rand.NewZipf(src.g.R, 1.1, 10, 199)
src.randDuration = rand.NewZipf(src.g.R, 1.1, 10, 9999)
// rest
src.record = make([]interface{}, len(src.schema))
src.record[0] = uint64(0)
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (t *TexasHealth) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (t *TexasHealth) DefaultEndAt() uint64 {
return 20000
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (t *TexasHealth) Info() string {
return "Generates sample data for the Texas Health POC."
}
// Ensure TexasHealthSource implements interface.
var _ idk.Source = (*TexasHealthSource)(nil)
// TexasHealthSource is an instance of a source generated
// by the Sourcer implementation TexasHleath.
type TexasHealthSource struct {
g *gen.Gen
cur uint64
endAt uint64
schema []idk.Field
record record
recordsGenForBucket int
currBucketSize int
currTimestamp time.Time
randBytes *rand.Zipf
randDuration *rand.Zipf
possibleEvents []ciscoNetworkEvent
possibleLogLevels []string
possibleHostnames []string
possibleIPAddrs []string
possiblePorts []string
possibleFromInterfaces []string
possibleToInterfaces []string
}
func (s *TexasHealthSource) genTimestamp() time.Time {
return s.currTimestamp
}
// advanceTick is a helper method to do some housekeeping
// after a record is generated. For one, it increments cur
// which keeps track of the number of records generated.
// Additionally, it advances the 'time-slot buckets' for
// lack of better naming. Each record generated belongs to
// a given time bucket and within that time bucket, all the
// records bear the same timestamp value (at second resolution).
// The bucket sizes are randomized and generally range between 1
// to 200. Currently, the bucket size value is generated randomly
// from a uniform distribution but a more realistic datagen should
// weight this value.
func (s *TexasHealthSource) advanceTick() {
s.cur++
s.recordsGenForBucket++
if s.recordsGenForBucket >= s.currBucketSize {
// reset bucket size to a random value between 1 and 200
s.currBucketSize = s.g.R.Intn(200) + 1
s.recordsGenForBucket = 0
oneSecond := time.Duration(1e9)
s.currTimestamp = s.currTimestamp.Add(oneSecond)
}
}
// Record implements idk.Source.
func (s *TexasHealthSource) Record() (idk.Record, error) {
if s.cur >= s.endAt {
return nil, io.EOF
}
selectEventIndex := s.g.R.Intn(len(s.possibleEvents))
event := s.possibleEvents[selectEventIndex]
logLevelIndex := s.g.R.Intn(len(s.possibleLogLevels))
timestamp := s.genTimestamp()
s.record[0] = s.cur // id
s.record[1] = event.id // event_id
s.record[2] = s.g.StringFromList(event.possibleTypes) // type
s.record[3] = event.protocol // protocol
s.record[4] = 3457177187 + s.cur // probe
s.record[5] = logLevelIndex + 1 // severity
s.record[6] = s.possibleLogLevels[logLevelIndex] // level
s.record[7] = s.g.StringFromList(s.possibleFromInterfaces) // from_interface
s.record[8] = s.g.StringFromListWeighted(s.possibleIPAddrs) // from_address
s.record[9] = s.g.StringFromListWeighted(s.possiblePorts) // from_port
s.record[10] = s.g.StringFromList(s.possibleToInterfaces) // to_interface
s.record[11] = s.g.StringFromListWeighted(s.possibleIPAddrs) // to_address
s.record[12] = s.g.StringFromListWeighted(s.possiblePorts) // to_port
s.record[13] = s.randDuration.Uint64() + 1 // duration_time
s.record[14] = s.randBytes.Uint64() + 1 // bytes
s.record[15] = s.g.StringFromList(s.possibleHostnames) // hostname
s.record[16] = timestamp.Unix() // timestamp
s.record[17] = timestamp.String() // timestamp_str
s.advanceTick()
return s.record, nil
}
// Schema implements idk.Source.
func (s *TexasHealthSource) Schema() []idk.Field {
return s.schema
}
func (s *TexasHealthSource) Close() error {
return nil
}

188
idk/datagen/timeseries.go Normal file
View file

@ -0,0 +1,188 @@
package datagen
import (
"io"
"math/rand"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
)
// Ensure Timeseries implements interface.
var _ Sourcer = (*Timeseries)(nil)
// Timeseries implements Sourcer.
type Timeseries struct {
schema []idk.Field
}
// NewTimeseries returns a new instance of Timeseries.
func NewTimeseries(cfg SourceGeneratorConfig) Sourcer {
return &Timeseries{
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.StringField{NameVal: "data_type"}, // 1
idk.IntField{NameVal: "equip_id"}, // 2
idk.StringField{NameVal: "group"}, // 3
idk.IntField{NameVal: "int_value"}, // 4
idk.StringField{NameVal: "string_value"}, // 5
idk.IntField{NameVal: "timestamp"}, // 6
idk.StringField{NameVal: "type"}, // 7
},
}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (t *Timeseries) Source(cfg SourceConfig) idk.Source {
src := &TimeseriesSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(22)),
schema: t.schema,
}
src.typeZipf = rand.NewZipf(src.rand, 1.03, 4, uint64(len(tsTypes))-1)
src.tsZipf = rand.NewZipf(src.rand, 3, 1.3, uint64(len(tsStrings))-1)
src.record = make([]interface{}, len(src.schema))
src.record[6] = int(1420070400 + cfg.startFrom)
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (t *Timeseries) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (t *Timeseries) DefaultEndAt() uint64 {
return 100000000
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (t *Timeseries) Info() string {
return "Generates timeseries device power state data (device, alert, status) - references data in sites, manufacturer, and equipment."
}
// Ensure TimeseriesSource implements interface.
var _ idk.Source = (*TimeseriesSource)(nil)
type TimeseriesSource struct {
Log logger.Logger
rand *rand.Rand
typeZipf *rand.Zipf
tsZipf *rand.Zipf
schema []idk.Field
cur, endAt uint64
record record
}
func NewTimeseriesSource(start, end uint64) *TimeseriesSource {
src := &TimeseriesSource{
cur: start,
endAt: end,
rand: rand.New(rand.NewSource(22)),
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.StringField{NameVal: "data_type"}, // 1
idk.IntField{NameVal: "equip_id"}, // 2
idk.StringField{NameVal: "group"}, // 3
idk.IntField{NameVal: "int_value"}, // 4
idk.StringField{NameVal: "string_value"}, // 5
idk.IntField{NameVal: "timestamp"}, // 6
idk.StringField{NameVal: "type"}, // 7
},
}
src.typeZipf = rand.NewZipf(src.rand, 1.03, 4, uint64(len(tsTypes))-1)
src.tsZipf = rand.NewZipf(src.rand, 3, 1.3, uint64(len(tsStrings))-1)
src.record = make([]interface{}, len(src.schema))
src.record[6] = int(1420070400 + start)
return src
}
func (s *TimeseriesSource) Record() (idk.Record, error) {
if s.cur >= s.endAt {
return nil, io.EOF
}
s.record[0] = s.cur
// type
s.record[7] = tsTypes[s.typeZipf.Uint64()]
groups, ok := groupsPerType[s.record[7].(string)]
if !ok {
s.Log.Printf("no %v in groups", s.record[7])
}
// group
if len(groups) != 0 {
s.record[3] = groups[s.rand.Intn(len(groups))]
} else {
s.record[3] = nil
}
// equip_id
s.record[2] = int64(s.rand.Intn(5000000))
// data type and string or int value
if s.rand.Intn(1000) > 995 {
s.record[1] = "string"
s.record[4] = nil
s.record[5] = tsStrings[s.tsZipf.Uint64()]
} else {
s.record[1] = "int"
s.record[4] = s.rand.Intn(100000)
s.record[5] = nil
}
// timestamp
s.record[6] = s.record[6].(int) + s.rand.Intn(3)
s.cur++
return s.record, nil
}
func (s *TimeseriesSource) Schema() []idk.Field {
return s.schema
}
func (s *TimeseriesSource) Seed(seed int64) {
s.rand.Seed(seed)
}
var _ Seedable = (*TimeseriesSource)(nil)
var tsStrings = []string{"on", "off", "alarm", "safe"}
var groupsPerType = map[string][]string{
"Voltage": {"calculated", "single", "moving"},
"VoltageRMS": {"calculated", "single", "moving"},
"Amperage": {"calculated", "single", "effective"},
"Resistance": {"fixed", "variable"},
"Frequency": {"sampled", "digital", "analog"},
"Message": {"event", "alert", "alarm"},
}
func (s *TimeseriesSource) Close() error {
return nil
}

5005
idk/datagen/titles.data.go Normal file

File diff suppressed because it is too large Load diff

207
idk/datagen/transactions.go Normal file
View file

@ -0,0 +1,207 @@
package datagen
import (
"io"
"math/rand"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
)
// Ensure Transaction implements interface.
var _ Sourcer = (*Transaction)(nil)
// Transaction implements Sourcer.
type Transaction struct {
schema []idk.Field
}
// NewTransaction returns a new instance of Transaction.
func NewTransaction(cfg SourceGeneratorConfig) Sourcer {
return &Transaction{
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.StringField{NameVal: "transaction_type"}, // 1
idk.StringField{NameVal: "payment_type"}, // 2
idk.IntField{NameVal: "site_id"}, // 3
idk.IntField{NameVal: "equip_id"}, // 4
idk.StringField{NameVal: "customer"}, // 5
idk.DecimalField{NameVal: "transaction_value", Scale: 2}, // 6
idk.IntField{NameVal: "timestamp"}, // 7
},
}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (t *Transaction) Source(cfg SourceConfig) idk.Source {
src := &TransactionSource{
cur: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(22)),
schema: t.schema,
}
src.typeZipf = rand.NewZipf(src.rand, 1.03, 4, uint64(len(tsTypes))-1)
src.instoreZipf = rand.NewZipf(src.rand, 1.3, 1.3, uint64(len(inStorePayments))-1)
src.ecomZipf = rand.NewZipf(src.rand, 1.3, 1.3, uint64(len(ecomPayments))-1)
src.valueZipf = rand.NewZipf(src.rand, 1.2, 4, 50000)
src.record = make([]interface{}, len(src.schema))
src.record[7] = int(1420070400 + cfg.startFrom)
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (t *Transaction) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (t *Transaction) DefaultEndAt() uint64 {
return 100000000
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (t *Transaction) Info() string {
return "Generates timeseries transaction data (transaction_amount, POS, site) - references data in sites, manufacturer, and equipment."
}
// Ensure TransactionSource implements interface.
var _ idk.Source = (*TransactionSource)(nil)
type TransactionSource struct {
Log logger.Logger
rand *rand.Rand
typeZipf *rand.Zipf
instoreZipf *rand.Zipf
ecomZipf *rand.Zipf
valueZipf *rand.Zipf
schema []idk.Field
cur, endAt uint64
record record
}
var transactionTypes = []string{"Ecommerce", "In-store"}
func (s *TransactionSource) Record() (idk.Record, error) {
if s.cur >= s.endAt {
return nil, io.EOF
}
s.record[0] = s.cur
s.cur++
// transaction type
transType := transactionTypes[s.rand.Intn(2)]
s.record[1] = transType
// payment type
paymentType := s.paymentType(transType)
s.record[2] = paymentType
// site id
s.record[3] = s.siteID(transType)
// equipment id
s.record[4] = s.equipID(transType)
// customer
s.record[5] = s.customer()
// transaction_value
s.record[6] = s.value(paymentType)
// timestamp
s.record[7] = s.record[7].(int) + s.rand.Intn(3)
return s.record, nil
}
func (s *TransactionSource) Schema() []idk.Field {
return s.schema
}
func (s *TransactionSource) Seed(seed int64) {
s.rand.Seed(seed)
}
var _ Seedable = (*TransactionSource)(nil)
func (s *TransactionSource) paymentType(transType string) string {
switch {
case transType == "Ecommerce":
return ecomPayments[s.ecomZipf.Uint64()]
case transType == "In-store":
return inStorePayments[s.instoreZipf.Uint64()]
default:
panic("unknown transaction type " + transType)
}
}
func (s *TransactionSource) siteID(transType string) int64 {
switch {
case transType == "Ecommerce":
num := s.rand.Int63n(500000)
if num < 400000 {
return num + 88000 // retail locations
} else {
return num + 4000000 // subset of data centers
}
case transType == "In-store":
return s.rand.Int63n(400000) + 88000
default:
panic("unknown transaction type (siteid)" + transType)
}
}
func (s *TransactionSource) equipID(transType string) int64 {
switch {
case transType == "Ecommerce":
return s.rand.Int63n(3000000) + 20000000 // subset of servers
case transType == "In-store":
return s.rand.Int63n(1000000) + 35000000 // POS devices
default:
panic("unknown transaction type (equipid)" + transType)
}
}
func (s *TransactionSource) customer() string {
return customers[s.rand.Intn(len(customers))]
}
func (s *TransactionSource) value(paymentType string) float64 {
val := float64(s.valueZipf.Uint64() + 15) // 15-50k
// 25% go lower ... to get some below $15
if s.rand.Intn(4) == 0 {
val -= float64(s.rand.Intn(14))
}
// half the time add cents
if s.rand.Intn(2) == 1 {
val = val + s.rand.Float64()
}
if paymentType == "Return" {
val = val * -1
}
return val
}
func (s *TransactionSource) Close() error {
return nil
}

View file

@ -0,0 +1,203 @@
package datagen
import (
"io"
"math/rand"
"github.com/molecula/featurebase/v3/idk"
"github.com/molecula/featurebase/v3/logger"
)
// Ensure Transaction1 implements interface.
var _ Sourcer = (*Transaction1)(nil)
// Transaction1 implements Sourcer.
type Transaction1 struct {
schema []idk.Field
}
// NewTransaction1 returns a new instance of Transaction1.
func NewTransaction1(cfg SourceGeneratorConfig) Sourcer {
return &Transaction1{
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.StringField{NameVal: "transaction_type"}, // 1
idk.StringField{NameVal: "payment_type"}, // 2
idk.IntField{NameVal: "site_id"}, // 3
idk.IntField{NameVal: "equip_id"}, // 4
idk.StringField{NameVal: "customer"}, // 5
idk.DecimalField{NameVal: "transaction_value", Scale: 2}, // 6
idk.IntField{NameVal: "timestamp"}, // 7
},
}
}
// Source returns an idk.Source which will generate
// records for a partition of the entire record space,
// determined by the concurrency value. It implements
// the Sourcer interface.
func (t *Transaction1) Source(cfg SourceConfig) idk.Source {
src := &Transaction1Source{
cur: cfg.startFrom,
endAt: cfg.endAt,
rand: rand.New(rand.NewSource(22)),
schema: t.schema,
}
src.instoreZipf = rand.NewZipf(src.rand, 1.3, 1.3, uint64(len(inStorePayments))-1)
src.ecomZipf = rand.NewZipf(src.rand, 1.3, 1.3, uint64(len(ecomPayments))-1)
src.valueZipf = rand.NewZipf(src.rand, 1.8, 4, 400)
src.record = make([]interface{}, len(src.schema))
src.record[7] = int(1509100000 + uint64(59.5*float64(cfg.startFrom)))
return src
}
// PrimaryKeyFields returns the fields from the schema
// which should be used as the index's primary key.
func (t *Transaction1) PrimaryKeyFields() []string {
return []string{"id"}
}
// DefaultEndAt sets the endAt record value for the
// case where one is not provided. It implements the
// Sourcer interface.
func (t *Transaction1) DefaultEndAt() uint64 {
return 23682459
}
// Info describes what this implementation of Sourcer
// generates. It implements the Sourcer interface.
func (t *Transaction1) Info() string {
return "TODO"
}
// Ensure Transaction1Source implements interface.
var _ idk.Source = (*Transaction1Source)(nil)
type Transaction1Source struct {
Log logger.Logger
cur, endAt uint64
rand *rand.Rand
instoreZipf *rand.Zipf
ecomZipf *rand.Zipf
valueZipf *rand.Zipf
schema []idk.Field
record record
}
func NewTransaction1Source(start, end uint64) *Transaction1Source {
src := &Transaction1Source{
cur: start,
endAt: end,
rand: rand.New(rand.NewSource(22)),
schema: []idk.Field{
idk.IDField{NameVal: "id"}, // 0
idk.StringField{NameVal: "transaction_type"}, // 1
idk.StringField{NameVal: "payment_type"}, // 2
idk.IntField{NameVal: "site_id"}, // 3
idk.IntField{NameVal: "equip_id"}, // 4
idk.StringField{NameVal: "customer"}, // 5
idk.DecimalField{NameVal: "transaction_value", Scale: 2}, // 6
idk.IntField{NameVal: "timestamp"}, // 7
},
}
src.instoreZipf = rand.NewZipf(src.rand, 1.3, 1.3, uint64(len(inStorePayments))-1)
src.ecomZipf = rand.NewZipf(src.rand, 1.3, 1.3, uint64(len(ecomPayments))-1)
src.valueZipf = rand.NewZipf(src.rand, 1.8, 4, 400)
src.record = make([]interface{}, len(src.schema))
src.record[7] = int(1509100000 + uint64(59.5*float64(start)))
return src
}
func (s *Transaction1Source) Record() (idk.Record, error) {
if s.cur >= s.endAt {
return nil, io.EOF
}
s.record[0] = 100000000 + s.cur
// timestamp
timestamp := s.record[7].(int) + s.rand.Intn(120)
if timestamp > 1509106300 {
return nil, io.EOF
}
s.record[7] = timestamp
// transaction type
transType := "In-store"
s.record[1] = transType
// payment type
paymentType := s.paymentType(transType)
s.record[2] = paymentType
// site id
s.record[3] = 88002 // Retail site which has breaker 712278
// equipment id
s.record[4] = []uint64{35100914, 35138991, 35144935, 35329399, 35894415}[s.cur%5] // POSs
// customer
s.record[5] = "CVS"
// transaction_value
s.record[6] = s.value(paymentType)
s.cur++
return s.record, nil
}
func (s *Transaction1Source) Schema() []idk.Field {
return s.schema
}
func (s *Transaction1Source) Seed(seed int64) {
s.rand.Seed(seed)
}
var _ Seedable = (*Transaction1Source)(nil)
func (s *Transaction1Source) paymentType(transType string) string {
switch {
case transType == "Ecommerce":
return ecomPayments[s.ecomZipf.Uint64()]
case transType == "In-store":
return inStorePayments[s.instoreZipf.Uint64()]
default:
panic("unknown transaction type " + transType)
}
}
func (s *Transaction1Source) value(paymentType string) float64 {
val := float64(s.valueZipf.Uint64() + 15) // 15-50k
// 25% go lower ... to get some below $15
if s.rand.Intn(4) == 0 {
val -= float64(s.rand.Intn(14))
}
// add cents
val = val + s.rand.Float64()
if paymentType == "Return" {
val = val * -1
}
return val
}
func (s *Transaction1Source) Close() error {
return nil
}

28342
idk/datagen/uscities.data.go Normal file

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more