Omniconf Clojure Configuration Library

repository·master·Indexed 41 days ago

https://github.com/grammarly/omniconf

A Clojure library that separates configuration blueprints (schemas) from values to provide unified, early-validated access to configuration. It supports multiple sources including CLI arguments, environment variables, Java properties, EDN files, and AWS Systems Manager (SSM) Parameter Store. Features include nested options, custom parsers, verifiers, and dynamic reconfiguration via SSM polling.

Tokens
3.1K
Snippets
13
Records
18
Agent score
2%

What's inside Omniconf

  1. Unset an Omniconf parameter via SSM

    master

    You can remove an existing Omniconf parameter by setting its corresponding SSM key to the special string value __SSM__UNSET__.

    Behavior Notes:

    • This works regardless of the parameter's type.
    • It performs a dissoc on the key in the current config map.
    • It does not revert the value to its original default; it completely removes the key-value pair from the map.
  2. How Omniconf works: Blueprint vs. Values

    master

    Omniconf operates on a core distinction between the blueprint and the values:

    1. Blueprint: A single, explicit declaration in your code that defines what configuration options exist, their types, defaults, and requirements. This prevents implicit configuration usage and allows for early validation.
    2. Values: The actual data provided by various sources (CLI, ENV, files, etc.).

    By separating these, Omniconf can unify disparate sources into a single, verified Clojure data structure. This ensures that all configuration is validated upfront before your application logic executes.

  3. Install Omniconf

    master

    Add Omniconf to your Clojure project dependencies via Clojars to begin using it for unified configuration management.

    <!-- Add to your deps.edn or project.clj using the latest version from Clojars -->
    https://clojars.org/com.grammarly/omniconf
  4. Run the Omniconf example project

    master

    To see Omniconf in action, navigate to the example directory and run the example core module using Clojure. You can pass configuration via environment variables (e.g., OPTION_FROM_SET) and specify required options using the --required-option flag.

    OPTION_FROM_SET=foo clojure -M -m example.core --required-option qux
  5. Provide configuration from EDN files

    master

    Omniconf can use EDN files as a configuration source. The file must contain a map of options to their values. Values can be provided in their native format (e.g., numbers, keywords) or as strings, in which case the Omniconf parser will be called on them.

    To handle dynamic filenames, a common pattern is to first call populate-from-cmd to extract a filename from CLI arguments, and then call populate-from-file. Note that populate-from-file will have priority over CLI arguments. If your CLI arguments are idempotent (i.e., they do not use special operations like ^:concat), you can call populate-from-cmd again after the file is loaded to restore CLI priority.

    If your EDN files use library-specific reader macros, you can provide data-readers via the *data-readers* binding during the populate-from-file call.

    (cfg/populate-from-cmd args) ;; args is a command-line arguments list
    (when-let [conf (cfg/get :conf)]
      (binding [cfg/*data-readers* {'ig/ref ig/ref 'ig/refset ig/refset}] ; e.g. reader macros used by integrant
        (cfg/populate-from-file conf)))
  6. Fetch configuration from AWS Systems Manager (SSM)

    master

    Omniconf supports Amazon SSM Parameter Store as a configuration source, which is ideal for managing secrets.

    Setup: You must add the omniconf.ssm dependency to your project.

    Usage: Use omniconf.core/populate-from-ssm with a path argument. This path acts as the root for nested parameters. For example, providing path /prod/myapp/ will map an SSM parameter /prod/myapp/db/password to the Omniconf key [:db :password].

    Mapping Strategies:

    1. Implicit Mapping: The structure of the SSM path is mirrored in the Omniconf configuration map.
    2. Explicit Mapping: Use the :ssm-name key in your configuration definition to map a specific Omniconf key to an absolute SSM path. These absolute paths ignore the path argument provided to populate-from-ssm.
    3. Relative Mapping: Use the ./ prefix in an :ssm-name to indicate the path is relative to the root path provided to populate-from-ssm.
    ;; Implicit mapping
    (cfg/define
      {:db {:nested {:password {:type :string
                                :secret true}}}})
    (cfg/populate-from-ssm "/prod/myapp/")
    
    ;; Explicit absolute mapping
    (cfg/define
      {:github-token {:type :string
                      :secret true
                      :ssm-name "/myteam/github/oauth-token"}})
    (cfg/populate-from-ssm "/prod/myapp/")
    
    ;; Relative mapping
    (cfg/define
      {:db {:nested {:password {:type :string
                                :secret true
                                :ssm-name "./db-pass"}}}})
    (cfg/populate-from-ssm "/prod/myapp/")
  7. Use special EDN operations: ^:concat and ^:merge

    master

    When setting values from any source (like CLI arguments), you can use special operations to append or merge data instead of overwriting it. These are attached to maps or lists using the ^:concat or ^:merge metadata.

    • ^:concat: Appends the new collection to the existing one.
    • ^:merge: Merges the new map into the existing one.
    $ my-app --emails '^:concat ["user1@corp.org"]' --roles '^:merge {"user1@corp.org" :user}'
  8. Configure custom logging for Omniconf

    master

    By default, Omniconf prints errors and the final configuration map to standard output. To redirect this output to a specific logging framework, use cfg/set-logging-fn and provide a function that accepts a vararg of arguments.

    (require '[taoensso.timbre :as log])
    (cfg/set-logging-fn (fn [& args] (log/info (str/join " " args))))
  9. Enable dynamic reconfiguration from SSM

    master

    To react to changes in AWS SSM Parameter Store without restarting your application, use populate-from-ssm-continually.

    This function polls SSM at a specified interval (in seconds). Omniconf will only report resetting the values that have actually changed.

    Warning: The :verifier step is not re-run after fetching updated values from SSM, so ensure your configuration invariants are not broken by dynamic updates.

    ;; Poll values under /prod/myapp/ prefix every 10 seconds.
    (cfg/populate-from-ssm-continually "/prod/myapp/" 10)
  10. Verify configuration with `cfg/verify`

    master

    Call cfg/verify to mark the boundary where configuration is guaranteed to be complete and correct.

    • Validation: If any required values are missing or types are incorrect, verify throws an exception. In non-REPL environments, stacktraces are stripped to show only a clean, human-readable error message.
    • Output: If successful, verify pretty-prints the entire configuration map to stdout. Use (cfg/verify :silent true) to suppress this output.
    (cfg/verify)
  11. Retrieve configuration values with `cfg/get`

    master

    Use cfg/get to extract values from the verified configuration. You can access nested values using a vector or as varargs.

    (cfg/get :hostname)           ; Single level
    (cfg/get :database :ip)       ; Nested via varargs
    (cfg/get [:database :ip])     ; Nested via vector
    (cfg/get :hostname)