Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(rt): slf4j 1.x compatability #963

Merged
merged 6 commits into from
Sep 27, 2023
Merged

fix(rt): slf4j 1.x compatability #963

merged 6 commits into from
Sep 27, 2023

Conversation

aajtodd
Copy link
Contributor

@aajtodd aajtodd commented Sep 26, 2023

Issue #

fixes awslabs/aws-sdk-kotlin#993

Description of changes

This PR provides automatic fallback to SLF4J 1.x compatible logging implementation when 2.x API can't be loaded. 2.x is binary compatible with 1.x but it does contain some new APIs that aren't available in 1.x (namely the fluent builder APIs). We could just map everything to a 1.x compatible API but it's not guaranteed to be the best mapping since underlying logging implementations may be more efficient than our compatibility layer can achieve.


Alternative

I was able to solve this differently by creating a separate 1.x implementation and then using Gradle component metadata rules to replace the underlying 2.x implementation we ship by default. I decided that fallback by default is a better customer experience though.

// file: build.gradle.kts of some consuming project

class LoggingCapability : ComponentMetadataRule {
    private val loggingModules = setOf("logging-slf4j2", "logging-slf4j")
    override fun execute(context: ComponentMetadataContext) = context.details.run {
        if (id.group == "aws.smithy.kotlin" && loggingModules.contains(id.name)){
            allVariants {
                withCapabilities {
                    addCapability("aws.smithy.kotlin", "slf4j-logger", id.version)
                }
            }
        }
    }
}

dependencies {
    // activate the "LoggingCapability" rule
    components.all(LoggingCapability::class.java)
    
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
    implementation("aws.sdk.kotlin:s3:$sdkVersion")
    implementation("aws.sdk.kotlin:dynamodb:$sdkVersion")

   // force 1.x on our classpath, this is just to reproduce the issue, in practice customers have been experiencing this
   // due to 1.x being what ends up on the classpath _somehow_ (e.g. shipped with gradle)
    implementation("org.slf4j:slf4j-api:1.7.36") {
        version {
            strictly("1.7.36")
        }
    }
    
    // hypothetical 1.x based implementation with same package name/types expected by default provider 
    implementation("aws.smithy.kotlin:logging-slf4j:$smithyKotlinVersion")

    // slf4j 1.x log4j
    implementation("org.apache.logging.log4j:log4j-slf4j-impl:2.20.0")
    implementation("org.apache.logging.log4j:log4j-core:2.20.0")
}

configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("aws.smithy.kotlin:slf4j-logger") {
        val toBeSelected = candidates.firstOrNull { it.id.let { id -> id is ModuleComponentIdentifier && id.module == "logging-slf4j" } }
        toBeSelected?.let { select(it) }
        because("need slf4j 1.x bindings")
    }
}

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
@aajtodd aajtodd requested a review from a team as a code owner September 26, 2023 18:23
@NatanLifshitz
Copy link
Contributor

Awesome, this issue was blocking us from updating past 0.28.1.

description = "Logging provider based on SLF4J 2"
description = "Logging provider based on SLF4J 2.x"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This description is no longer fully accurate. It's based on 2.x unless it's not.

Comment on lines +50 to +52
override fun setContext(context: Context) {
// TODO - add a way to get the current trace context and set the key/value pair on it?
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could be moved down to AbstractSlf4jLoggerAdapter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately it can't because this is on the LogRecordBuilder implementation which is the thing that differs between the two.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, sorry I was thinking this was still in Slf4j1xLoggerAdapter. Nvm.

Comment on lines 16 to 22
private val slf4jLoggerAdapter: (org.slf4j.Logger) -> Logger = try {
Class.forName("org.slf4j.spi.LoggingEventBuilder")
::Slf4j2xLoggerAdapter
} catch (ex: ClassNotFoundException) {
LoggerFactory.getLogger(Slf4jLoggerProvider::class.java).warn("falling back to SLF4J 1.x compatible binding")
::Slf4j1xLoggerAdapter
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: May be clearer as a function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not opposed but can you clarify what you are referring to? What would you prefer to see here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A class-level val that returns a function is more complex to read than simply a fun:

private fun slf4jLoggerAdapter(delegate: org.slf4j.Logger) = try {
    Class.forName("org.slf4j.spi.LoggingEventBuilder")
    Slf4j2xLoggerAdapter(delegate)
} catch (_: ClassNotFoundException) {
    LoggerFactory.getLogger(Slf4jLoggerProvider::class.java).warn("falling back to SLF4J 1.x compatible binding")
    Slf4j1xLoggerAdapter(delegate)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to avoid invoking the class loader on every logger instantiation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could refactor it though to something like:

private val slf4j2Found = try {
    Class.forName("org.slf4j.spi.LoggingEventBuilder")
    true
} catch(_: ClassNotFoundException) {
     LoggerFactory.getLogger(Slf4jLoggerProvider::class.java).warn("falling back to SLF4J 1.x compatible binding")
    false  
}

   override fun getOrCreateLogger(name: String): Logger {
        val sl4fjLogger = LoggerFactory.getLogger(name)
        return if (slf4j2Found) { 
             Slf4j2xLoggerAdapter(slf4jLogger)
        } else {
             Slf4j1xLoggerAdapter(slf4jLogger)
        }
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right. I didn't think about caching the class loader invocation. OK, your original code works better then.

@sonarqubecloud
Copy link

SonarCloud Quality Gate failed.    Quality Gate failed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 2 Code Smells

No Coverage information No Coverage information
9.8% 9.8% Duplication

idea Catch issues before they fail your Quality Gate with our IDE extension sonarlint SonarLint

description = "Logging provider based on SLF4J 2"
extra["displayName"] = "Smithy :: Kotlin :: Observability :: SLF4J2 binding"
description = "Logging provider based on SLF4J"
extra["displayName"] = "Smithy :: Kotlin :: Observability :: SLF4J binding"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the directory logging-slf4j2 also be renamed to drop the 2?

@aajtodd aajtodd merged commit 08721de into main Sep 27, 2023
@aajtodd aajtodd deleted the slf4j-1x branch September 27, 2023 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Upgrading to 0.29.0-Beta introduces slf4j dependency hell: NoSuchMethodError
4 participants